diff --git a/monkey/common/configuration/agent_sub_configuration_schemas.py b/monkey/common/configuration/agent_sub_configuration_schemas.py index ceec0af24..4d2ee2d8e 100644 --- a/monkey/common/configuration/agent_sub_configuration_schemas.py +++ b/monkey/common/configuration/agent_sub_configuration_schemas.py @@ -1,7 +1,4 @@ from marshmallow import Schema, fields, post_load -from marshmallow_enum import EnumField - -from common import OperatingSystems from .agent_sub_configurations import ( CustomPBAConfiguration, @@ -87,7 +84,6 @@ class ExploitationOptionsConfigurationSchema(Schema): class ExploiterConfigurationSchema(Schema): name = fields.Str() options = fields.Mapping() - supported_os = fields.List(EnumField(OperatingSystems)) @post_load def _make_exploiter_configuration(self, data, **kwargs): diff --git a/monkey/common/configuration/agent_sub_configurations.py b/monkey/common/configuration/agent_sub_configurations.py index 560542e1d..c4a0c704c 100644 --- a/monkey/common/configuration/agent_sub_configurations.py +++ b/monkey/common/configuration/agent_sub_configurations.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import Dict, List -from common import OperatingSystems - @dataclass(frozen=True) class CustomPBAConfiguration: @@ -54,7 +52,6 @@ class ExploitationOptionsConfiguration: class ExploiterConfiguration: name: str options: Dict - supported_os: List[OperatingSystems] @dataclass(frozen=True) diff --git a/monkey/common/configuration/default_agent_configuration.py b/monkey/common/configuration/default_agent_configuration.py index 8c909f15d..c83169566 100644 --- a/monkey/common/configuration/default_agent_configuration.py +++ b/monkey/common/configuration/default_agent_configuration.py @@ -157,60 +157,44 @@ DEFAULT_AGENT_CONFIGURATION_JSON = """{ "brute_force": [ { "name": "MSSQLExploiter", - "options": {}, - "supported_os": [ - "WINDOWS" - ] + "options": {} + }, { "name": "PowerShellExploiter", - "options": {}, - "supported_os": [ - "WINDOWS" - ] + "options": {} + }, { "name": "SSHExploiter", - "options": {}, - "supported_os": [ - "LINUX" - ] + "options": {} + }, { "name": "SmbExploiter", "options": { "smb_download_timeout": 30 - }, - "supported_os": [ - "WINDOWS" - ] + } + }, { "name": "WmiExploiter", "options": { "smb_download_timeout": 30 - }, - "supported_os": [ - "WINDOWS" - ] + } + } ], "vulnerability": [ { "name": "HadoopExploiter", - "options": {}, - "supported_os": [ - "LINUX", - "WINDOWS" - ] + "options": {} + }, { "name": "Log4ShellExploiter", - "options": {}, - "supported_os": [ - "LINUX", - "WINDOWS" - ] + "options": {} + } ] } diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index b9eada5b6..35a212482 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -7,6 +7,7 @@ from queue import Queue from threading import Event from typing import Callable, Dict, List, Mapping +from common import OperatingSystems from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost @@ -20,6 +21,18 @@ ExploiterName = str Callback = Callable[[ExploiterName, VictimHost, ExploiterResultData], None] +SUPPORTED_OS = { + "HadoopExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], + "Log4ShellExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], + "MSSQLExploiter": [OperatingSystems.WINDOWS], + "PowerShellExploiter": [OperatingSystems.WINDOWS], + "SSHExploiter": [OperatingSystems.LINUX], + "SmbExploiter": [OperatingSystems.WINDOWS], + "WmiExploiter": [OperatingSystems.WINDOWS], + "ZerologonExploiter": [OperatingSystems.WINDOWS], +} + + class Exploiter: def __init__( self, @@ -118,7 +131,8 @@ class Exploiter: victim_os = victim_host.os.get("type") # We want to try all exploiters if the victim's OS is unknown - if victim_os is not None and victim_os not in exploiter["supported_os"]: + print(victim_os) + if victim_os is not None and victim_os not in SUPPORTED_OS[exploiter_name]: logger.debug( f"Skipping {exploiter_name} because it does not support " f"the victim's OS ({victim_os})" diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 9db06d486..e2253b36c 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -12,6 +12,7 @@ from common import DIContainer from monkey_island.cc.database import database, mongo from monkey_island.cc.resources import AgentBinaries, RemoteRun from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.resources.agent_configuration import AgentConfiguration from monkey_island.cc.resources.agent_controls import StopAgentCheck, StopAllAgents from monkey_island.cc.resources.attack.attack_report import AttackReport from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt @@ -155,6 +156,7 @@ def init_api_resources(api: FlaskDIWrapper): api.add_resource(IslandConfiguration) api.add_resource(ConfigurationExport) api.add_resource(ConfigurationImport) + api.add_resource(AgentConfiguration) api.add_resource(AgentBinaries) api.add_resource(NetMap) api.add_resource(Edge) diff --git a/monkey/monkey_island/cc/resources/agent_configuration.py b/monkey/monkey_island/cc/resources/agent_configuration.py new file mode 100644 index 000000000..f0ad73cb8 --- /dev/null +++ b/monkey/monkey_island/cc/resources/agent_configuration.py @@ -0,0 +1,36 @@ +import json + +import marshmallow +from flask import make_response, request + +from common.configuration.agent_configuration import AgentConfigurationSchema +from monkey_island.cc.repository import IAgentConfigurationRepository +from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.resources.request_authentication import jwt_required + + +class AgentConfiguration(AbstractResource): + urls = ["/api/agent-configuration"] + + def __init__(self, agent_configuration_repository: IAgentConfigurationRepository): + self._agent_configuration_repository = agent_configuration_repository + self._schema = AgentConfigurationSchema() + + @jwt_required + def get(self): + configuration = self._agent_configuration_repository.get_configuration() + configuration_json = self._schema.dumps(configuration) + return make_response(configuration_json, 200) + + @jwt_required + def post(self): + + try: + configuration_object = self._schema.loads(request.data) + self._agent_configuration_repository.store_configuration(configuration_object) + return make_response({}, 200) + except (marshmallow.exceptions.ValidationError, json.JSONDecodeError) as err: + return make_response( + {"message": f"Invalid configuration supplied: {err}"}, + 400, + ) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 86541c33d..69db1c2e1 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -3,12 +3,10 @@ import copy import functools import logging import re -from itertools import chain from typing import Any, Dict, List from jsonschema import Draft4Validator, validators -from common import OperatingSystems from common.config_value_paths import ( LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, @@ -580,7 +578,7 @@ class ConfigService: formatted_exploiters_config = ConfigService._add_smb_download_timeout_to_exploiters( formatted_exploiters_config ) - return ConfigService._add_supported_os_to_exploiters(formatted_exploiters_config) + return formatted_exploiters_config @staticmethod def _add_smb_download_timeout_to_exploiters( @@ -593,23 +591,3 @@ class ConfigService: exploiter["options"]["smb_download_timeout"] = SMB_DOWNLOAD_TIMEOUT return new_config - - @staticmethod - def _add_supported_os_to_exploiters( - formatted_config: Dict, - ) -> Dict[str, List[Dict[str, Any]]]: - supported_os = { - "HadoopExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], - "Log4ShellExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], - "MSSQLExploiter": [OperatingSystems.WINDOWS], - "PowerShellExploiter": [OperatingSystems.WINDOWS], - "SSHExploiter": [OperatingSystems.LINUX], - "SmbExploiter": [OperatingSystems.WINDOWS], - "WmiExploiter": [OperatingSystems.WINDOWS], - "ZerologonExploiter": [OperatingSystems.WINDOWS], - } - new_config = copy.deepcopy(formatted_config) - for exploiter in chain(new_config["brute_force"], new_config["vulnerability"]): - exploiter["supported_os"] = supported_os.get(exploiter["name"], []) - - return new_config diff --git a/monkey/tests/common/example_agent_configuration.py b/monkey/tests/common/example_agent_configuration.py index 640771df4..25a1dbd5e 100644 --- a/monkey/tests/common/example_agent_configuration.py +++ b/monkey/tests/common/example_agent_configuration.py @@ -39,18 +39,16 @@ NETWORK_SCAN_CONFIGURATION = { } BRUTE_FORCE = [ - {"name": "ex1", "options": {}, "supported_os": ["LINUX"]}, + {"name": "ex1", "options": {}}, { "name": "ex2", "options": {"smb_download_timeout": 10}, - "supported_os": ["LINUX", "WINDOWS"], }, ] VULNERABILITY = [ { "name": "ex3", "options": {"smb_download_timeout": 10}, - "supported_os": ["WINDOWS"], }, ] EXPLOITATION_CONFIGURATION = { diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index 7fcc2285d..c0c035b9d 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -47,16 +47,16 @@ "exploiters": { "options": {}, "brute_force": [ - {"name": "MSSQLExploiter", "supported_os": ["windows"], "options": {}}, - {"name": "PowerShellExploiter", "supported_os": ["windows"], "options": {}}, - {"name": "SmbExploiter", "supported_os": ["windows"], "options": {}}, - {"name": "SSHExploiter", "supported_os": ["linux"], "options": {}}, - {"name": "WmiExploiter", "supported_os": ["windows"], "options": {}} + {"name": "MSSQLExploiter", "options": {}}, + {"name": "PowerShellExploiter", "options": {}}, + {"name": "SmbExploiter", "options": {}}, + {"name": "SSHExploiter", "options": {}}, + {"name": "WmiExploiter", "options": {}} ], "vulnerability": [ - {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, - {"name": "ShellShockExploiter", "supported_os": ["linux"], "options": {}}, - {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}} + {"name": "HadoopExploiter", "options": {}}, + {"name": "ShellShockExploiter", "options": {}}, + {"name": "ZerologonExploiter", "options": {}} ] } }, diff --git a/monkey/tests/monkey_island/__init__.py b/monkey/tests/monkey_island/__init__.py index aed4d07f3..8f3654a6e 100644 --- a/monkey/tests/monkey_island/__init__.py +++ b/monkey/tests/monkey_island/__init__.py @@ -1,3 +1,4 @@ from .single_file_repository import SingleFileRepository from .mock_file_repository import MockFileRepository, FILE_CONTENTS, FILE_NAME from .open_error_file_repository import OpenErrorFileRepository +from .in_memory_agent_configuration_repository import InMemoryAgentConfigurationRepository diff --git a/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py b/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py new file mode 100644 index 000000000..e737d645c --- /dev/null +++ b/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py @@ -0,0 +1,15 @@ +from tests.common.example_agent_configuration import AGENT_CONFIGURATION + +from common.configuration.agent_configuration import AgentConfigurationSchema +from monkey_island.cc.repository import IAgentConfigurationRepository + + +class InMemoryAgentConfigurationRepository(IAgentConfigurationRepository): + def __init__(self): + self._configuration = AgentConfigurationSchema().load(AGENT_CONFIGURATION) + + def get_configuration(self): + return self._configuration + + def store_configuration(self, agent_configuration): + self._configuration = agent_configuration diff --git a/monkey/tests/unit_tests/common/test_agent_configuration.py b/monkey/tests/unit_tests/common/test_agent_configuration.py index 3383ab1b7..7ea80cfc5 100644 --- a/monkey/tests/unit_tests/common/test_agent_configuration.py +++ b/monkey/tests/unit_tests/common/test_agent_configuration.py @@ -23,7 +23,6 @@ from tests.common.example_agent_configuration import ( WINDOWS_FILENAME, ) -from common import OperatingSystems from common.configuration import ( DEFAULT_AGENT_CONFIGURATION_JSON, AgentConfiguration, @@ -126,16 +125,12 @@ def test_exploitation_options_configuration_schema(): def test_exploiter_configuration_schema(): name = "bond" options = {"gun": "Walther PPK", "car": "Aston Martin DB5"} - supported_os = [OperatingSystems.LINUX, OperatingSystems.WINDOWS] schema = ExploiterConfigurationSchema() - config = schema.load( - {"name": name, "options": options, "supported_os": [os_.name for os_ in supported_os]} - ) + config = schema.load({"name": name, "options": options}) assert config.name == name assert config.options == options - assert config.supported_os == supported_os def test_exploitation_configuration(): diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py index a339ec17f..e99385c5b 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py @@ -2,6 +2,7 @@ from unittest.mock import Mock import pytest +from common import OperatingSystems from infection_monkey.exploit.tools.helpers import ( AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64, @@ -13,22 +14,28 @@ from infection_monkey.exploit.tools.helpers import ( def _get_host(os): host = Mock() host.os = {"type": os} + host.is_windows = lambda: os == OperatingSystems.WINDOWS return host @pytest.mark.parametrize( - "os, path", [("linux", AGENT_BINARY_PATH_LINUX), ("windows", AGENT_BINARY_PATH_WIN64)] + "os, path", + [ + (OperatingSystems.LINUX, AGENT_BINARY_PATH_LINUX), + (OperatingSystems.WINDOWS, AGENT_BINARY_PATH_WIN64), + ], ) def test_get_agent_dst_path(os, path): host = _get_host(os) rand_path = get_agent_dst_path(host) + print(f"{os}: {rand_path}") # Assert that filename got longer by RAND_SUFFIX_LEN and one dash assert len(str(rand_path)) == (len(str(path)) + RAND_SUFFIX_LEN + 1) def test_get_agent_dst_path_randomness(): - host = _get_host("windows") + host = _get_host(OperatingSystems.WINDOWS) path1 = get_agent_dst_path(host) path2 = get_agent_dst_path(host) @@ -37,7 +44,7 @@ def test_get_agent_dst_path_randomness(): def test_get_agent_dst_path_str_place(): - host = _get_host("windows") + host = _get_host(OperatingSystems.WINDOWS) rand_path = get_agent_dst_path(host) 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 c804b60e5..f5d9c2da4 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -39,24 +39,12 @@ def exploiter_config(): return { "options": {"dropper_path_linux": "/tmp/monkey"}, "brute_force": [ - { - "name": "HadoopExploiter", - "supported_os": [OperatingSystems.WINDOWS], - "options": {"timeout": 10}, - }, - {"name": "SSHExploiter", "supported_os": [OperatingSystems.LINUX], "options": {}}, - { - "name": "WmiExploiter", - "supported_os": [OperatingSystems.WINDOWS], - "options": {"timeout": 10}, - }, + {"name": "MSSQLExploiter", "options": {"timeout": 10}}, + {"name": "SSHExploiter", "options": {}}, + {"name": "WmiExploiter", "options": {"timeout": 10}}, ], "vulnerability": [ - { - "name": "ZerologonExploiter", - "supported_os": [OperatingSystems.WINDOWS], - "options": {}, - }, + {"name": "ZerologonExploiter", "options": {}}, ], } @@ -118,11 +106,11 @@ def test_exploiter(callback, hosts, hosts_to_exploit, run_exploiters): host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list) assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos - assert ("HadoopExploiter", hosts[0]) in host_exploit_combos + assert ("MSSQLExploiter", hosts[0]) in host_exploit_combos assert ("SSHExploiter", hosts[0]) in host_exploit_combos assert ("WmiExploiter", hosts[0]) in host_exploit_combos assert ("ZerologonExploiter", hosts[1]) in host_exploit_combos - assert ("HadoopExploiter", hosts[1]) in host_exploit_combos + assert ("MSSQLExploiter", hosts[1]) in host_exploit_combos assert ("WmiExploiter", hosts[1]) in host_exploit_combos assert ("SSHExploiter", hosts[1]) in host_exploit_combos @@ -209,6 +197,6 @@ def test_all_exploiters_run_on_unknown_host(callback, hosts, hosts_to_exploit, r host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list) assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos - assert ("HadoopExploiter", hosts[0]) in host_exploit_combos + assert ("MSSQLExploiter", hosts[0]) in host_exploit_combos assert ("SSHExploiter", host) in host_exploit_combos assert ("WmiExploiter", hosts[0]) in host_exploit_combos diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_configuration.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_configuration.py new file mode 100644 index 000000000..874500b81 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_configuration.py @@ -0,0 +1,50 @@ +import json + +import pytest +from tests.common import StubDIContainer +from tests.common.example_agent_configuration import AGENT_CONFIGURATION +from tests.monkey_island import InMemoryAgentConfigurationRepository +from tests.unit_tests.monkey_island.conftest import get_url_for_resource + +from monkey_island.cc.repository import IAgentConfigurationRepository +from monkey_island.cc.resources.agent_configuration import AgentConfiguration + + +@pytest.fixture +def flask_client(build_flask_client): + container = StubDIContainer() + + container.register(IAgentConfigurationRepository, InMemoryAgentConfigurationRepository) + + with build_flask_client(container) as flask_client: + yield flask_client + + +def test_agent_configuration_endpoint(flask_client): + agent_configuration_url = get_url_for_resource(AgentConfiguration) + + flask_client.post( + agent_configuration_url, data=json.dumps(AGENT_CONFIGURATION), follow_redirects=True + ) + resp = flask_client.get(agent_configuration_url) + + assert resp.status_code == 200 + assert json.loads(resp.data) == AGENT_CONFIGURATION + + +def test_agent_configuration_invalid_config(flask_client): + agent_configuration_url = get_url_for_resource(AgentConfiguration) + + resp = flask_client.post( + agent_configuration_url, data=json.dumps({"invalid_config": "invalid_stuff"}) + ) + + assert resp.status_code == 400 + + +def test_agent_configuration_invalid_json(flask_client): + agent_configuration_url = get_url_for_resource(AgentConfiguration) + + resp = flask_client.post(agent_configuration_url, data="InvalidJson!") + + assert resp.status_code == 400 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index df866e388..85f3f4823 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -1,6 +1,5 @@ import pytest -from common import OperatingSystems from monkey_island.cc.services.config import ConfigService # If tests fail because config path is changed, sync with @@ -172,38 +171,32 @@ def test_format_config_for_agent__exploiters(): "http_ports": [80, 443, 7001, 8008, 8080, 9200], }, "brute_force": [ - {"name": "MSSQLExploiter", "supported_os": [OperatingSystems.WINDOWS], "options": {}}, + {"name": "MSSQLExploiter", "options": {}}, { "name": "PowerShellExploiter", - "supported_os": [OperatingSystems.WINDOWS], "options": {}, }, - {"name": "SSHExploiter", "supported_os": [OperatingSystems.LINUX], "options": {}}, + {"name": "SSHExploiter", "options": {}}, { "name": "SmbExploiter", - "supported_os": [OperatingSystems.WINDOWS], "options": {"smb_download_timeout": 30}, }, { "name": "WmiExploiter", - "supported_os": [OperatingSystems.WINDOWS], "options": {"smb_download_timeout": 30}, }, ], "vulnerability": [ { "name": "HadoopExploiter", - "supported_os": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], "options": {}, }, { "name": "Log4ShellExploiter", - "supported_os": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], "options": {}, }, { "name": "ZerologonExploiter", - "supported_os": [OperatingSystems.WINDOWS], "options": {}, }, ],