diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 7cee1e6fe..478cb6660 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -7,27 +7,125 @@ weight: 4 tags: ["setup", "docker", "linux", "windows"] --- +## Supported operating systems + +The Infection Monkey Docker container works on Linux only. It is not compatible with Docker for Windows or Docker for Mac. + ## Deployment -### Linux +### 1. Load the docker images +1. Pull the MongoDB v4.2 Docker image: -To extract the `tar.gz` file, run `tar -xvzf monkey-island-docker.tar.gz`. + ```bash + sudo docker pull mongo:4.2 + ``` -Once you've extracted the container from the tar.gz file, run the following commands: +1. Extract the Monkey Island Docker tarball: -```sh -sudo docker load -i dk.monkeyisland.1.10.0.tar -sudo docker pull mongo:4.2 -sudo mkdir -p /var/monkey-mongo/data/db -sudo docker run --name monkey-mongo --network=host -v /var/monkey-mongo/data/db:/data/db -d mongo:4.2 -sudo docker run --name monkey-island --network=host -d guardicore/monkey-island:1.10.0 -``` + ```bash + tar -xvzf monkey-island-docker.tar.gz + ``` -Wait until the Island is done setting up and it will be available on https://localhost:5000 +1. Load the Monkey Island Docker image: -### Windows and Mac OS X + ```bash + sudo docker load -i dk.monkeyisland.1.10.0.tar + ``` -Not supported yet, since docker doesn't support `--network=host` parameter on these OS's. +### 2. Start MongoDB + +1. Start a MongoDB Docker container: + + ```bash + sudo docker run \ + --name monkey-mongo \ + --network=host \ + --volume db:/data/db \ + --detach mongo:4.2 + ``` + +### 3a. Start Monkey Island with default certificate + +By default, Infection Monkey comes with a [self-signed SSL certificate](https://aboutssl.org/what-is-self-sign-certificate/). In +enterprise or other security-sensitive environments, it is recommended that the +user [provide Infection Monkey with a +certificate](#3b-start-monkey-island-with-user-provided-certificate) that has +been signed by a private certificate authority. + +1. Run the Monkey Island server + ```bash + sudo docker run \ + --name monkey-island \ + --network=host \ + guardicore/monkey-island:1.10.0 + ``` + +### 3b. Start Monkey Island with user-provided certificate + +1. Create a directory named `monkey_island_data`. This will serve as the + location where Infection Monkey stores its configuration and runtime + artifacts. + + ```bash + mkdir ./monkey_island_data + ``` + +1. Run Monkey Island with the `--setup-only` flag to populate the `./monkey_island_data` directory with a default `server_config.json` file. + + ```bash + sudo docker run \ + --rm \ + --name monkey-island \ + --network=host \ + --user $(id -u ${USER}):$(id -g ${USER}) \ + --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ + guardicore/monkey-island:1.10.0 --setup-only + ``` + +1. (Optional but recommended) Move your `.crt` and `.key` files to `./monkey_island_data`. + +1. Make sure that your `.crt` and `.key` files are read-only and readable only by you. + + ```bash + chmod 400 + chmod 400 + ``` + +1. Edit `./monkey_island_data/server_config.json` to configure Monkey Island + to use your certificate. Your config should look something like this: + + ```json {linenos=inline,hl_lines=["11-14"]} + { + "data_dir": "/monkey_island_data", + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "docker" + }, + "mongodb": { + "start_mongodb": false + }, + "ssl_certificate": { + "ssl_certificate_file": "", + "ssl_certificate_key_file": "", + } + } + ``` + +1. Start the Monkey Island server: + + ```bash + sudo docker run \ + --name monkey-island \ + --network=host \ + --user $(id -u ${USER}):$(id -g ${USER}) \ + --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ + guardicore/monkey-island:1.10.0 + ``` + +### 4. Accessing Monkey Island + +After the Monkey Island docker container starts, you can access Monkey Island by pointing your browser at `https://localhost:5000`. ## Upgrading @@ -43,12 +141,27 @@ using the *Export config* button and then import it to the new Monkey Island. ## Troubleshooting ### The Monkey Island container crashes due to a 'UnicodeDecodeError' -`UnicodeDecodeError: 'utf-8' codec can't decode byte 0xee in position 0: invalid continuation byte` -You may encounter this error because of the existence of different MongoDB keys in the `monkey-island` and `monkey-mongo` containers. +You will encounter a `UnicodeDecodeError` if the `monkey-island` container is +using a different secret key to encrypt sensitive data than was initially used +to store data in the `monkey-mongo` container. -Starting a new container from the `guardicore/monkey-island:1.10.0` image generates a new secret key for storing sensitive information in MongoDB. If you have an old database instance running (from a previous run of Monkey), the key in the `monkey-mongo` container is different than the newly generated key in the `monkey-island` container. Since encrypted data (obtained from the previous run) is stored in MongoDB with the old key, decryption fails and you get this error. +``` +UnicodeDecodeError: 'utf-8' codec can't decode byte 0xee in position 0: invalid continuation byte +``` -You can fix this in two ways: +Starting a new container from the `guardicore/monkey-island:1.10.0` image +generates a new secret key for storing sensitive information in MongoDB. If you +have an old database instance running (from a previous instance of Infection +Monkey), the data stored in the `monkey-mongo` container has been encrypted +with a key that is different from the one that Monkey Island is currently +using. When MongoDB attempts to decrypt its data with the new key, decryption +fails and you get this error. + +You can fix this in one of three ways: 1. Instead of starting a new container for the Monkey Island, you can run `docker container start -a monkey-island` to restart the existing container, which will contain the correct key material. -2. Kill and remove the existing MongoDB container, and start a new one. This will remove the old database entirely. Then, start the new Monkey Island container. +1. Kill and remove the existing MongoDB container, and start a new one. This will remove the old database entirely. Then, start the new Monkey Island container. +1. When you start the Monkey Island container, use `--volume + monkey_island_data:/monkey_island_data`. This will store all of Monkey + Island's runtime artifacts (including the encryption key file) in a docker + volume that can be reused by subsequent Monkey Island containers. diff --git a/docs/content/setup/linux.md b/docs/content/setup/linux.md new file mode 100644 index 000000000..85e4a0f13 --- /dev/null +++ b/docs/content/setup/linux.md @@ -0,0 +1,81 @@ +--- +title: "Linux" +date: 2020-05-26T20:57:28+03:00 +draft: false +pre: ' ' +weight: 4 +tags: ["setup", "AppImage", "linux"] +--- + +## Supported operating systems + +## Deployment + +1. Make the AppImage package executable: + ```bash + chmod u+x Infection_Monkey_v1.11.0.AppImage + ``` +1. Start Monkey Island by running the Infection Monkey AppImage package: + ```bash + ./Infection_Monkey_v1.11.0.AppImage + ``` +1. Access the Monkey Island web UI by pointing your browser at + `https://localhost:5000`. + +### Start Monkey Island with user-provided certificate + +By default, Infection Monkey comes with a [self-signed SSL +certificate](https://aboutssl.org/what-is-self-sign-certificate/). In +enterprise or other security-sensitive environments, it is recommended that the +user provide Infection Monkey with a certificate that has been signed by a +private certificate authority. + +1. Run the Infection Monkey AppImage package with the `--setup-only` flag to + populate the `$HOME/.monkey_island` directory with a default + `server_config.json` file. + + ```bash + ./Infection_Monkey_v1.11.0.AppImage --setup-only + ``` + +1. (Optional but recommended) Move your `.crt` and `.key` files to + `$HOME/.monkey_island`. + +1. Make sure that your `.crt` and `.key` files are read-only and readable only + by you. + + ```bash + chmod 400 + chmod 400 + ``` + +1. Edit `$HOME/.monkey_island/server_config.json` to configure Monkey Island + to use your certificate. Your config should look something like this: + + ```json {linenos=inline,hl_lines=["11-14"]} + { + "data_dir": "~/.monkey_island", + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "linux" + }, + "mongodb": { + "start_mongodb": true + }, + "ssl_certificate": { + "ssl_certificate_file": "", + "ssl_certificate_key_file": "", + } + } + ``` + +1. Start Monkey Island by running the Infection Monkey AppImage package: + ```bash + ./Infection_Monkey_v1.11.0.AppImage + ``` + +1. Access the Monkey Island web UI by pointing your browser at + `https://localhost:5000`. + +## Upgrading diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 8396b423b..632c08991 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -52,3 +52,7 @@ class FindingWithoutDetailsError(Exception): class DomainControllerNameFetchError(FailedExploitationError): """ Raise on failed attempt to extract domain controller's name """ + + +class InsecurePermissionsError(Exception): + """ Raise when a file does not have permissions that are secure enough """ diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 4eaa13131..ee1774240 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -1,6 +1,5 @@ import json import logging -import os import sys from pathlib import Path from threading import Thread @@ -22,12 +21,12 @@ from monkey_island.cc.arg_parser import IslandCmdArgs # noqa: E402 from monkey_island.cc.arg_parser import parse_cli_args # noqa: E402 from monkey_island.cc.resources.monkey_download import MonkeyDownload # noqa: E402 from monkey_island.cc.server_utils.bootloader_server import BootloaderHttpServer # noqa: E402 -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH # noqa: E402 from monkey_island.cc.server_utils.encryptor import initialize_encryptor # noqa: E402 from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402 from monkey_island.cc.services.initialize import initialize_services # noqa: E402 from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list # noqa: E402 from monkey_island.cc.services.utils.network_utils import local_ip_addresses # noqa: E402 +from monkey_island.cc.setup import island_config_options_validator # noqa: E402 from monkey_island.cc.setup.island_config_options import IslandConfigOptions # noqa: E402 from monkey_island.cc.setup.mongo.database_initializer import init_collections # noqa: E402 from monkey_island.cc.setup.mongo.mongo_setup import ( # noqa: E402 @@ -44,6 +43,8 @@ def run_monkey_island(): island_args = parse_cli_args() config_options, server_config_path = _setup_data_dir(island_args) + _exit_on_invalid_config_options(config_options) + _configure_logging(config_options) _initialize_globals(config_options, server_config_path) @@ -67,6 +68,14 @@ def _setup_data_dir(island_args: IslandCmdArgs) -> Tuple[IslandConfigOptions, st exit(1) +def _exit_on_invalid_config_options(config_options: IslandConfigOptions): + try: + island_config_options_validator.raise_on_invalid_options(config_options) + except Exception as ex: + print(f"Configuration error: {ex}") + exit(1) + + def _configure_logging(config_options): reset_logger() setup_logging(config_options.data_dir, config_options.log_level) @@ -83,9 +92,6 @@ def _start_island_server(should_setup_only, config_options: IslandConfigOptions) populate_exporter_list() app = init_app(MONGO_URL) - crt_path = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.crt")) - key_path = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.key")) - init_collections() if should_setup_only: @@ -94,14 +100,23 @@ def _start_island_server(should_setup_only, config_options: IslandConfigOptions) bootloader_server_thread = _start_bootloader_server() + logger.info( + f"Using certificate path: {config_options.crt_path}, and key path: " + "{config_options.key_path}." + ) + if env_singleton.env.is_debug(): - app.run(host="0.0.0.0", debug=True, ssl_context=(crt_path, key_path)) + app.run( + host="0.0.0.0", + debug=True, + ssl_context=(config_options.crt_path, config_options.key_path), + ) else: http_server = WSGIServer( ("0.0.0.0", env_singleton.env.get_island_port()), app, - certfile=os.environ.get("SERVER_CRT", crt_path), - keyfile=os.environ.get("SERVER_KEY", key_path), + certfile=config_options.crt_path, + keyfile=config_options.key_path, ) _log_init_info() http_server.serve_forever() diff --git a/monkey/monkey_island/cc/server_utils/consts.py b/monkey/monkey_island/cc/server_utils/consts.py index 357e2bc8e..ef5d0733c 100644 --- a/monkey/monkey_island/cc/server_utils/consts.py +++ b/monkey/monkey_island/cc/server_utils/consts.py @@ -2,6 +2,7 @@ import os from pathlib import Path from monkey_island.cc.environment.utils import is_windows_os +from monkey_island.cc.server_utils import file_utils __author__ = "itay.mizeretz" @@ -25,7 +26,7 @@ SERVER_CONFIG_FILENAME = "server_config.json" MONKEY_ISLAND_ABS_PATH = _get_monkey_island_abs_path() -DEFAULT_DATA_DIR = os.path.expandvars(get_default_data_dir()) +DEFAULT_DATA_DIR = file_utils.expand_path(get_default_data_dir()) DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5 @@ -36,9 +37,18 @@ MONGO_EXECUTABLE_PATH = ( _MONGO_EXECUTABLE_PATH_WIN if is_windows_os() else _MONGO_EXECUTABLE_PATH_LINUX ) -DEFAULT_SERVER_CONFIG_PATH = os.path.expandvars( +DEFAULT_SERVER_CONFIG_PATH = file_utils.expand_path( os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", SERVER_CONFIG_FILENAME) ) DEFAULT_LOG_LEVEL = "INFO" + DEFAULT_START_MONGO_DB = True + +DEFAULT_CRT_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.crt")) +DEFAULT_KEY_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.key")) + +DEFAULT_CERTIFICATE_PATHS = { + "ssl_certificate_file": DEFAULT_CRT_PATH, + "ssl_certificate_key_file": DEFAULT_KEY_PATH, +} diff --git a/monkey/monkey_island/cc/server_utils/file_utils.py b/monkey/monkey_island/cc/server_utils/file_utils.py new file mode 100644 index 000000000..668aa4356 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/file_utils.py @@ -0,0 +1,57 @@ +import os + +from monkey_island.cc.environment.utils import is_windows_os + + +def expand_path(path: str) -> str: + return os.path.expandvars(os.path.expanduser(path)) + + +def has_expected_permissions(path: str, expected_permissions: int) -> bool: + if is_windows_os(): + return _has_expected_windows_permissions(path, expected_permissions) + + return _has_expected_linux_permissions(path, expected_permissions) + + +def _has_expected_linux_permissions(path: str, expected_permissions: int) -> bool: + file_mode = os.stat(path).st_mode + file_permissions = file_mode & 0o777 + + return file_permissions == expected_permissions + + +def _has_expected_windows_permissions(path: str, expected_permissions: int) -> bool: + import win32api # noqa: E402 + import win32security # noqa: E402 + + FULL_CONTROL = 2032127 + ACE_TYPE_ALLOW = 0 + ACE_TYPE_DENY = 1 + + admins_sid, _, _ = win32security.LookupAccountName("", "Administrators") + user_sid, _, _ = win32security.LookupAccountName("", win32api.GetUserName()) + + security_descriptor = win32security.GetNamedSecurityInfo( + path, win32security.SE_FILE_OBJECT, win32security.DACL_SECURITY_INFORMATION + ) + + acl = security_descriptor.GetSecurityDescriptorDacl() + + for i in range(acl.GetAceCount()): + ace = acl.GetAce(i) + ace_type, _ = ace[0] # 0 for allow, 1 for deny + permissions = ace[1] + sid = ace[-1] + + if sid == user_sid: + if not (permissions == expected_permissions and ace_type == ACE_TYPE_ALLOW): + return False + elif sid == admins_sid: + continue + # TODO: consider removing; so many system accounts/groups exist, it's likely to fail + else: + if not (permissions == FULL_CONTROL and ace_type == ACE_TYPE_DENY): + return False + + return True diff --git a/monkey/monkey_island/cc/setup/config_setup.py b/monkey/monkey_island/cc/setup/config_setup.py index 601c67efc..d1e3e984b 100644 --- a/monkey/monkey_island/cc/setup/config_setup.py +++ b/monkey/monkey_island/cc/setup/config_setup.py @@ -1,9 +1,9 @@ -import os from typing import Tuple from monkey_island.cc.arg_parser import IslandCmdArgs from monkey_island.cc.environment import server_config_handler from monkey_island.cc.environment.utils import create_secure_directory +from monkey_island.cc.server_utils import file_utils from monkey_island.cc.server_utils.consts import DEFAULT_SERVER_CONFIG_PATH from monkey_island.cc.setup.island_config_options import IslandConfigOptions @@ -16,14 +16,19 @@ def setup_data_dir(island_args: IslandCmdArgs) -> Tuple[IslandConfigOptions, str def _setup_config_by_cmd_arg(server_config_path) -> Tuple[IslandConfigOptions, str]: - server_config_path = os.path.expandvars(os.path.expanduser(server_config_path)) + server_config_path = file_utils.expand_path(server_config_path) config = server_config_handler.load_server_config_from_file(server_config_path) create_secure_directory(config.data_dir, create_parent_dirs=True) return config, server_config_path def _setup_default_config() -> Tuple[IslandConfigOptions, str]: - config = server_config_handler.load_server_config_from_file(DEFAULT_SERVER_CONFIG_PATH) - create_secure_directory(config.data_dir, create_parent_dirs=False) - server_config_path = server_config_handler.create_default_server_config_file(config.data_dir) + default_config = server_config_handler.load_server_config_from_file(DEFAULT_SERVER_CONFIG_PATH) + default_data_dir = default_config.data_dir + + create_secure_directory(default_data_dir, create_parent_dirs=False) + + server_config_path = server_config_handler.create_default_server_config_file(default_data_dir) + config = server_config_handler.load_server_config_from_file(server_config_path) + return config, server_config_path diff --git a/monkey/monkey_island/cc/setup/island_config_options.py b/monkey/monkey_island/cc/setup/island_config_options.py index 5ce62ba2e..9704e5f45 100644 --- a/monkey/monkey_island/cc/setup/island_config_options.py +++ b/monkey/monkey_island/cc/setup/island_config_options.py @@ -1,21 +1,33 @@ from __future__ import annotations -import os - from monkey_island.cc.server_utils.consts import ( + DEFAULT_CERTIFICATE_PATHS, + DEFAULT_CRT_PATH, DEFAULT_DATA_DIR, + DEFAULT_KEY_PATH, DEFAULT_LOG_LEVEL, DEFAULT_START_MONGO_DB, ) +from monkey_island.cc.server_utils.file_utils import expand_path class IslandConfigOptions: def __init__(self, config_contents: dict): - self.data_dir = os.path.expandvars( - os.path.expanduser(config_contents.get("data_dir", DEFAULT_DATA_DIR)) - ) + self.data_dir = expand_path(config_contents.get("data_dir", DEFAULT_DATA_DIR)) + self.log_level = config_contents.get("log_level", DEFAULT_LOG_LEVEL) self.start_mongodb = config_contents.get( "mongodb", {"start_mongodb": DEFAULT_START_MONGO_DB} ).get("start_mongodb", DEFAULT_START_MONGO_DB) + + self.crt_path = expand_path( + config_contents.get("ssl_certificate", DEFAULT_CERTIFICATE_PATHS).get( + "ssl_certificate_file", DEFAULT_CRT_PATH + ) + ) + self.key_path = expand_path( + config_contents.get("ssl_certificate", DEFAULT_CERTIFICATE_PATHS).get( + "ssl_certificate_key_file", DEFAULT_KEY_PATH + ) + ) diff --git a/monkey/monkey_island/cc/setup/island_config_options_validator.py b/monkey/monkey_island/cc/setup/island_config_options_validator.py new file mode 100644 index 000000000..5febe16ab --- /dev/null +++ b/monkey/monkey_island/cc/setup/island_config_options_validator.py @@ -0,0 +1,34 @@ +import os + +from common.utils.exceptions import InsecurePermissionsError +from monkey_island.cc.environment.utils import is_windows_os +from monkey_island.cc.server_utils.file_utils import has_expected_permissions +from monkey_island.cc.setup.island_config_options import IslandConfigOptions + + +def raise_on_invalid_options(options: IslandConfigOptions): + LINUX_READ_ONLY_BY_USER = 0o400 + WINDOWS_READ_ONLY = 1179817 + + _raise_if_not_isfile(options.crt_path) + _raise_if_incorrect_permissions(options.crt_path, LINUX_READ_ONLY_BY_USER, WINDOWS_READ_ONLY) + + _raise_if_not_isfile(options.key_path) + _raise_if_incorrect_permissions(options.key_path, LINUX_READ_ONLY_BY_USER, WINDOWS_READ_ONLY) + + +def _raise_if_not_isfile(f: str): + if not os.path.isfile(f): + raise FileNotFoundError(f"{f} does not exist or is not a regular file.") + + +def _raise_if_incorrect_permissions( + f: str, linux_expected_permissions: int, windows_expected_permissions: int +): + expected_permissions = ( + windows_expected_permissions if is_windows_os() else linux_expected_permissions + ) + if not has_expected_permissions(f, expected_permissions): + raise InsecurePermissionsError( + f"The file {f} has incorrect permissions. Expected: {expected_permissions}" + ) diff --git a/monkey/monkey_island/linux/create_certificate.sh b/monkey/monkey_island/linux/create_certificate.sh index ca7d397e0..cbbe5261b 100644 --- a/monkey/monkey_island/linux/create_certificate.sh +++ b/monkey/monkey_island/linux/create_certificate.sh @@ -21,10 +21,16 @@ umask 377 echo "Generating key in $server_root/server.key..." openssl genrsa -out "$server_root"/server.key 2048 +chmod 400 "$server_root"/server.key + echo "Generating csr in $server_root/server.csr..." openssl req -new -key "$server_root"/server.key -out "$server_root"/server.csr -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" +chmod 400 "$server_root"/server.csr + echo "Generating certificate in $server_root/server.crt..." openssl x509 -req -days 366 -in "$server_root"/server.csr -signkey "$server_root"/server.key -out "$server_root"/server.crt +chmod 400 "$server_root"/server.crt + # Shove some new random data into the file to override the original seed we put in. if [ "$CREATED_RND_FILE" = true ] ; then diff --git a/monkey/monkey_island/windows/create_certificate.bat b/monkey/monkey_island/windows/create_certificate.bat index 645c6fa25..3062f5c57 100644 --- a/monkey/monkey_island/windows/create_certificate.bat +++ b/monkey/monkey_island/windows/create_certificate.bat @@ -16,3 +16,17 @@ copy "%mydir%windows\openssl.cfg" "%mydir%bin\openssl\openssl.cfg" "%mydir%bin\openssl\openssl.exe" genrsa -out "%mydir%cc\server.key" 1024 "%mydir%bin\openssl\openssl.exe" req -new -config "%mydir%bin\openssl\openssl.cfg" -key "%mydir%cc\server.key" -out "%mydir%cc\server.csr" -subj "/OU=Monkey Department/CN=monkey.com" "%mydir%bin\openssl\openssl.exe" x509 -req -days 366 -in "%mydir%cc\server.csr" -signkey "%mydir%cc\server.key" -out "%mydir%cc\server.crt" + + +:: Change file permissions +SET adminsIdentity="BUILTIN\Administrators" +FOR /f %%O IN ('whoami') DO SET ownIdentity=%%O + +FOR %%F IN ("%mydir%cc\server.key", "%mydir%cc\server.csr", "%mydir%cc\server.crt") DO ( + + :: Remove all others and add admins rule (with full control) + echo y| cacls %%F" /p %adminsIdentity%:F + + :: Add user rule (with read) + echo y| cacls %%F /e /p %ownIdentity%:R +) diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_file_utils.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_file_utils.py new file mode 100644 index 000000000..6297aada9 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_file_utils.py @@ -0,0 +1,52 @@ +import os +import subprocess + +import pytest + +from monkey_island.cc.server_utils import file_utils + + +def test_expand_user(patched_home_env): + input_path = os.path.join("~", "test") + expected_path = os.path.join(patched_home_env, "test") + + assert file_utils.expand_path(input_path) == expected_path + + +def test_expand_vars(patched_home_env): + input_path = os.path.join("$HOME", "test") + expected_path = os.path.join(patched_home_env, "test") + + assert file_utils.expand_path(input_path) == expected_path + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_has_expected_permissions_true_linux(tmpdir, create_empty_tmp_file): + file_name = create_empty_tmp_file("test") + os.chmod(file_name, 0o754) + + assert file_utils.has_expected_permissions(file_name, 0o754) + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_has_expected_permissions_false_linux(tmpdir, create_empty_tmp_file): + file_name = create_empty_tmp_file("test") + os.chmod(file_name, 0o755) + + assert not file_utils.has_expected_permissions(file_name, 0o700) + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_has_expected_permissions_true_windows(tmpdir, create_empty_tmp_file): + file_name = create_empty_tmp_file("test") + subprocess.run(f"echo y| cacls {file_name} /p %USERNAME%:F", shell=True) # noqa: DUO116 + + assert file_utils.has_expected_permissions(file_name, 2032127) + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_has_expected_permissions_false_windows(tmpdir, create_empty_tmp_file): + file_name = create_empty_tmp_file("test") + subprocess.run(f"echo y| cacls {file_name} /p %USERNAME%:R", shell=True) # noqa: DUO116 + + assert not file_utils.has_expected_permissions(file_name, 2032127) diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py index ae26d5145..1554980a2 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py @@ -1,7 +1,9 @@ import os from monkey_island.cc.server_utils.consts import ( + DEFAULT_CRT_PATH, DEFAULT_DATA_DIR, + DEFAULT_KEY_PATH, DEFAULT_LOG_LEVEL, DEFAULT_START_MONGO_DB, ) @@ -11,6 +13,10 @@ TEST_CONFIG_FILE_CONTENTS_SPECIFIED = { "data_dir": "/tmp", "log_level": "test", "mongodb": {"start_mongodb": False}, + "ssl_certificate": { + "ssl_certificate_file": "/tmp/test.crt", + "ssl_certificate_key_file": "/tmp/test.key", + }, } TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED = {} @@ -18,54 +24,122 @@ TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED = {} TEST_CONFIG_FILE_CONTENTS_NO_STARTMONGO = {"mongodb": {}} -def test_island_config_options__data_dir_specified(): - assert_island_config_options_data_dir_equals(TEST_CONFIG_FILE_CONTENTS_SPECIFIED, "/tmp") +def test_data_dir_specified(): + assert_data_dir_equals(TEST_CONFIG_FILE_CONTENTS_SPECIFIED, "/tmp") -def test_island_config_options__data_dir_uses_default(): - assert_island_config_options_data_dir_equals( - TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED, DEFAULT_DATA_DIR - ) +def test_data_dir_uses_default(): + assert_data_dir_equals(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED, DEFAULT_DATA_DIR) -def test_island_config_options__data_dir_expanduser(monkeypatch, tmpdir): - set_home_env(monkeypatch, tmpdir) +def test_data_dir_expanduser(patched_home_env): DATA_DIR_NAME = "test_data_dir" - assert_island_config_options_data_dir_equals( - {"data_dir": os.path.join("~", DATA_DIR_NAME)}, os.path.join(tmpdir, DATA_DIR_NAME) + assert_data_dir_equals( + {"data_dir": os.path.join("~", DATA_DIR_NAME)}, + os.path.join(patched_home_env, DATA_DIR_NAME), ) -def test_island_config_options__data_dir_expandvars(monkeypatch, tmpdir): - set_home_env(monkeypatch, tmpdir) +def test_data_dir_expandvars(patched_home_env): DATA_DIR_NAME = "test_data_dir" - assert_island_config_options_data_dir_equals( - {"data_dir": os.path.join("$HOME", DATA_DIR_NAME)}, os.path.join(tmpdir, DATA_DIR_NAME) + assert_data_dir_equals( + {"data_dir": os.path.join("$HOME", DATA_DIR_NAME)}, + os.path.join(patched_home_env, DATA_DIR_NAME), ) -def set_home_env(monkeypatch, tmpdir): - monkeypatch.setenv("HOME", str(tmpdir)) +def assert_data_dir_equals(config_file_contents, expected_data_dir): + assert_island_config_option_equals(config_file_contents, "data_dir", expected_data_dir) -def assert_island_config_options_data_dir_equals(config_file_contents, expected_data_dir): - options = IslandConfigOptions(config_file_contents) - assert options.data_dir == expected_data_dir - - -def test_island_config_options__log_level(): +def test_log_level(): options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_SPECIFIED) assert options.log_level == "test" options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED) assert options.log_level == DEFAULT_LOG_LEVEL -def test_island_config_options__mongodb(): +def test_mongodb(): options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_SPECIFIED) assert not options.start_mongodb options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED) assert options.start_mongodb == DEFAULT_START_MONGO_DB options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_NO_STARTMONGO) assert options.start_mongodb == DEFAULT_START_MONGO_DB + + +def test_crt_path_uses_default(): + assert_ssl_certificate_file_equals(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED, DEFAULT_CRT_PATH) + + +def test_crt_path_specified(): + assert_ssl_certificate_file_equals( + TEST_CONFIG_FILE_CONTENTS_SPECIFIED, + TEST_CONFIG_FILE_CONTENTS_SPECIFIED["ssl_certificate"]["ssl_certificate_file"], + ) + + +def test_crt_path_expanduser(patched_home_env): + FILE_NAME = "test.crt" + + assert_ssl_certificate_file_equals( + {"ssl_certificate": {"ssl_certificate_file": os.path.join("~", FILE_NAME)}}, + os.path.join(patched_home_env, FILE_NAME), + ) + + +def test_crt_path_expandvars(patched_home_env): + FILE_NAME = "test.crt" + + assert_ssl_certificate_file_equals( + {"ssl_certificate": {"ssl_certificate_file": os.path.join("$HOME", FILE_NAME)}}, + os.path.join(patched_home_env, FILE_NAME), + ) + + +def assert_ssl_certificate_file_equals(config_file_contents, expected_ssl_certificate_file): + assert_island_config_option_equals( + config_file_contents, "crt_path", expected_ssl_certificate_file + ) + + +def test_key_path_uses_default(): + assert_ssl_certificate_key_file_equals(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED, DEFAULT_KEY_PATH) + + +def test_key_path_specified(): + assert_ssl_certificate_key_file_equals( + TEST_CONFIG_FILE_CONTENTS_SPECIFIED, + TEST_CONFIG_FILE_CONTENTS_SPECIFIED["ssl_certificate"]["ssl_certificate_key_file"], + ) + + +def test_key_path_expanduser(patched_home_env): + FILE_NAME = "test.key" + + assert_ssl_certificate_key_file_equals( + {"ssl_certificate": {"ssl_certificate_key_file": os.path.join("~", FILE_NAME)}}, + os.path.join(patched_home_env, FILE_NAME), + ) + + +def test_key_path_expandvars(patched_home_env): + FILE_NAME = "test.key" + + assert_ssl_certificate_key_file_equals( + {"ssl_certificate": {"ssl_certificate_key_file": os.path.join("$HOME", FILE_NAME)}}, + os.path.join(patched_home_env, FILE_NAME), + ) + + +def assert_ssl_certificate_key_file_equals(config_file_contents, expected_ssl_certificate_file): + assert_island_config_option_equals( + config_file_contents, "key_path", expected_ssl_certificate_file + ) + + +def assert_island_config_option_equals(config_file_contents, option_name, expected_value): + options = IslandConfigOptions(config_file_contents) + assert getattr(options, option_name) == expected_value diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options_validator.py b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options_validator.py new file mode 100644 index 000000000..b6d9eeb85 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options_validator.py @@ -0,0 +1,158 @@ +import os +import subprocess +from collections.abc import Callable + +import pytest + +from common.utils.exceptions import InsecurePermissionsError +from monkey_island.cc.setup.island_config_options import IslandConfigOptions +from monkey_island.cc.setup.island_config_options_validator import raise_on_invalid_options + +LINUX_READ_ONLY_BY_USER = 0o400 +LINUX_RWX_BY_ALL = 0o777 + + +def certificate_test_island_config_options(crt_file, key_file): + return IslandConfigOptions( + { + "ssl_certificate": { + "ssl_certificate_file": crt_file, + "ssl_certificate_key_file": key_file, + } + } + ) + + +@pytest.fixture +def linux_island_config_options(create_read_only_linux_file: Callable): + crt_file = create_read_only_linux_file("test.crt") + key_file = create_read_only_linux_file("test.key") + + return certificate_test_island_config_options(crt_file, key_file) + + +@pytest.fixture +def create_read_only_linux_file(tmpdir: str, create_empty_tmp_file: Callable) -> Callable: + def inner(file_name: str) -> str: + full_file_path = create_empty_tmp_file(file_name) + os.chmod(full_file_path, LINUX_READ_ONLY_BY_USER) + + return full_file_path + + return inner + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_valid_crt_and_key_paths(linux_island_config_options): + try: + raise_on_invalid_options(linux_island_config_options) + except Exception as ex: + print(ex) + assert False + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_crt_path_does_not_exist(linux_island_config_options): + os.remove(linux_island_config_options.crt_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(linux_island_config_options) + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_crt_path_insecure_permissions(linux_island_config_options): + os.chmod(linux_island_config_options.crt_path, LINUX_RWX_BY_ALL) + + with pytest.raises(InsecurePermissionsError): + raise_on_invalid_options(linux_island_config_options) + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_key_path_does_not_exist(linux_island_config_options): + os.remove(linux_island_config_options.key_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(linux_island_config_options) + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_key_path_insecure_permissions(linux_island_config_options): + os.chmod(linux_island_config_options.key_path, LINUX_RWX_BY_ALL) + + with pytest.raises(InsecurePermissionsError): + raise_on_invalid_options(linux_island_config_options) + + +@pytest.fixture +def windows_island_config_options(tmpdir: str, create_read_only_windows_file: Callable): + crt_file = create_read_only_windows_file("test.crt") + key_file = create_read_only_windows_file("test.key") + + return certificate_test_island_config_options(crt_file, key_file) + + +@pytest.fixture +def create_read_only_windows_file(tmpdir: str, create_empty_tmp_file: Callable) -> Callable: + def inner(file_name: str) -> str: + full_file_path = create_empty_tmp_file(file_name) + cmd_to_change_permissions = get_windows_cmd_to_change_permissions(full_file_path, "R") + subprocess.run(cmd_to_change_permissions, shell=True) # noqa DUO116 + + return full_file_path + + return inner + + +def get_windows_cmd_to_change_permissions(file_name, permissions): + """ + :param file_name: name of file + :param permissions: can be: N (None), R (Read), W (Write), C (Change (write)), F (Full control) + """ + return f"echo y| cacls {file_name} /p %USERNAME%:{permissions}" + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_valid_crt_and_key_paths(windows_island_config_options): + try: + raise_on_invalid_options(windows_island_config_options) + except Exception as ex: + print(ex) + assert False + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_crt_path_does_not_exist(windows_island_config_options): + os.remove(windows_island_config_options.crt_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(windows_island_config_options) + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_crt_path_insecure_permissions(windows_island_config_options): + cmd_to_change_permissions = get_windows_cmd_to_change_permissions( + windows_island_config_options.crt_path, "W" + ) + subprocess.run(cmd_to_change_permissions, shell=True) # noqa DUO116 + + with pytest.raises(InsecurePermissionsError): + raise_on_invalid_options(windows_island_config_options) + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_key_path_does_not_exist(windows_island_config_options): + os.remove(windows_island_config_options.key_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(windows_island_config_options) + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_key_path_insecure_permissions(windows_island_config_options): + cmd_to_change_permissions = get_windows_cmd_to_change_permissions( + windows_island_config_options.key_path, "W" + ) + subprocess.run(cmd_to_change_permissions, shell=True) # noqa DUO116 + + with pytest.raises(InsecurePermissionsError): + raise_on_invalid_options(windows_island_config_options) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 037bee72b..538576aef 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -1,4 +1,5 @@ import os +from collections.abc import Callable import pytest @@ -6,3 +7,22 @@ import pytest @pytest.fixture(scope="module") def server_configs_dir(data_for_tests_dir): return os.path.join(data_for_tests_dir, "server_configs") + + +@pytest.fixture +def patched_home_env(monkeypatch, tmpdir): + monkeypatch.setenv("HOME", str(tmpdir)) + + return tmpdir + + +@pytest.fixture +def create_empty_tmp_file(tmpdir: str) -> Callable: + def inner(file_name: str): + new_file = os.path.join(tmpdir, file_name) + with open(new_file, "w"): + pass + + return new_file + + return inner