Merge branch 'custom-certificate' into develop

This commit is contained in:
Mike Salvatore 2021-06-09 14:57:28 -04:00
commit 1c5daf168c
15 changed files with 716 additions and 61 deletions

View File

@ -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 <PATH_TO_KEY_FILE>
chmod 400 <PATH_TO_CRT_FILE>
```
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": "<PATH_TO_CRT_FILE>",
"ssl_certificate_key_file": "<PATH_TO_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.

View File

@ -0,0 +1,81 @@
---
title: "Linux"
date: 2020-05-26T20:57:28+03:00
draft: false
pre: '<i class="fab fa-linux"></i> '
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 <PATH_TO_KEY_FILE>
chmod 400 <PATH_TO_CRT_FILE>
```
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": "<PATH_TO_CRT_FILE>",
"ssl_certificate_key_file": "<PATH_TO_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

View File

@ -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 """

View File

@ -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()

View File

@ -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,
}

View File

@ -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

View File

@ -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

View File

@ -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
)
)

View File

@ -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}"
)

View File

@ -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

View File

@ -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
)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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