diff --git a/monkey/monkey_island/cc/resources/reset_agent_configuration.py b/monkey/monkey_island/cc/resources/reset_agent_configuration.py index d2d394a02..36b49820e 100644 --- a/monkey/monkey_island/cc/resources/reset_agent_configuration.py +++ b/monkey/monkey_island/cc/resources/reset_agent_configuration.py @@ -2,22 +2,22 @@ from http import HTTPStatus from flask import make_response -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 +from monkey_island.cc.services import RepositoryService class ResetAgentConfiguration(AbstractResource): urls = ["/api/reset-agent-configuration"] - def __init__(self, agent_configuration_repository: IAgentConfigurationRepository): - self._agent_configuration_repository = agent_configuration_repository + def __init__(self, repository_service: RepositoryService): + self._repository_service = repository_service @jwt_required def post(self): """ Reset the agent configuration to its default values """ - self._agent_configuration_repository.reset_to_default() + self._repository_service.reset_agent_configuration() return make_response({}, HTTPStatus.OK) diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 14c5664e9..9be3ac250 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -2,3 +2,4 @@ from .authentication_service import AuthenticationService from .aws import AWSService from .island_mode_service import IslandModeService +from .repository_service import RepositoryService diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 68fc46aac..240002e04 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -138,11 +138,6 @@ class ConfigService: ConfigService.set_config_value(PBA_LINUX_FILENAME_PATH, linux_filename) ConfigService.set_config_value(PBA_WINDOWS_FILENAME_PATH, windows_filename) - @staticmethod - def init_config(): - if ConfigService.get_config(should_decrypt=False) != {}: - return - @staticmethod def decrypt_config(config): ConfigService._encrypt_or_decrypt_config(config, True) diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py index 3dd512026..a73e182f5 100644 --- a/monkey/monkey_island/cc/services/database.py +++ b/monkey/monkey_island/cc/services/database.py @@ -6,11 +6,11 @@ from monkey_island.cc.database import mongo from monkey_island.cc.models import Config from monkey_island.cc.models.agent_controls import AgentControls from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations -from monkey_island.cc.services.config import ConfigService logger = logging.getLogger(__name__) +# NOTE: This service is being replaced by the RepositoryService class Database(object): def __init__(self): pass @@ -24,7 +24,6 @@ class Database(object): for x in mongo.db.collection_names() if Database._should_drop(x, reset_config) ] - ConfigService.init_config() Database.init_agent_controls() logger.info("DB was reset") return jsonify(status="OK") diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index a622b091c..9cf019421 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -32,8 +32,7 @@ from monkey_island.cc.repository import ( ) from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.encryption import ILockableEncryptor, RepositoryEncryptor -from monkey_island.cc.services import AWSService, IslandModeService -from monkey_island.cc.services.post_breach_files import PostBreachFilesService +from monkey_island.cc.services import AWSService, IslandModeService, RepositoryService from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( CredentialsParser, @@ -70,7 +69,6 @@ def initialize_services(data_dir: Path) -> DIContainer: _patch_credentials_parser(container) # This is temporary until we get DI all worked out. - PostBreachFilesService.initialize(container.resolve(IFileRepository)) ReportService.initialize(container.resolve(AWSService)) return container @@ -148,6 +146,7 @@ def _register_services(container: DIContainer): container.register_instance(LocalMonkeyRunService, container.resolve(LocalMonkeyRunService)) container.register_instance(IslandModeService, container.resolve(IslandModeService)) container.register_instance(AuthenticationService, container.resolve(AuthenticationService)) + container.register_instance(RepositoryService, container.resolve(RepositoryService)) def _patch_credentials_parser(container: DIContainer): diff --git a/monkey/monkey_island/cc/services/post_breach_files.py b/monkey/monkey_island/cc/services/post_breach_files.py deleted file mode 100644 index efb370b31..000000000 --- a/monkey/monkey_island/cc/services/post_breach_files.py +++ /dev/null @@ -1,24 +0,0 @@ -import logging - -from monkey_island.cc.repository import IFileRepository - -logger = logging.getLogger(__name__) - - -# TODO: This service wraps an IFileStorageService for the sole purpose of making the -# `remove_PBA_files()` method available to the ConfigService. This whole service can be -# removed once ConfigService is refactored to be stateful (it already is but everything is -# still statically/globally scoped) and use dependency injection. -class PostBreachFilesService: - _file_storage_service = None - - # TODO: A number of these services should be instance objects instead of - # static/singleton hybrids. At the moment, this requires invasive refactoring that's - # not a priority. - @classmethod - def initialize(cls, file_storage_service: IFileRepository): - cls._file_storage_service = file_storage_service - - @classmethod - def remove_PBA_files(cls): - cls._file_storage_service.delete_all_files() diff --git a/monkey/monkey_island/cc/services/repository_service.py b/monkey/monkey_island/cc/services/repository_service.py new file mode 100644 index 000000000..af0d13fa1 --- /dev/null +++ b/monkey/monkey_island/cc/services/repository_service.py @@ -0,0 +1,40 @@ +from monkey_island.cc.repository import IAgentConfigurationRepository, IFileRepository + + +class RepositoryService: + def __init__( + self, + agent_configuration_repository: IAgentConfigurationRepository, + file_repository: IFileRepository, + ): + self._agent_configuration_repository = agent_configuration_repository + self._file_repository = file_repository + + def reset_agent_configuration(self): + # NOTE: This method will be replaced by an event when we implement pub/sub in the island. + # Different plugins and components will be able to register for the event and reset + # their configurations. + self._remove_pba_files() + self._agent_configuration_repository.reset_to_default() + + def _remove_pba_files(self): + agent_configuration = self._agent_configuration_repository.get_configuration() + custom_pbas = agent_configuration.custom_pbas + + if custom_pbas.linux_filename: + self._file_repository.delete_file(custom_pbas.linux_filename) + + if custom_pbas.windows_filename: + self._file_repository.delete_file(custom_pbas.windows_filename) + + def unlock(self): + raise NotImplementedError + + def reset_key(self): + raise NotImplementedError + + def clear_simulation_data(self): + # NOTE: This method will be replaced by an event when we implement pub/sub in the island. + # Different plugins and components will be able to register for the event and clear + # any configuration data they've collected. + raise NotImplementedError diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index a10823564..ead85a236 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -235,18 +235,9 @@ class ConfigurePageComponent extends AuthComponent { this.props.onStatusChange(); } ) - .then(() => { - this.removePBAfile(API_PBA_WINDOWS, this.setPbaFilenameWindows) - this.removePBAfile(API_PBA_LINUX, this.setPbaFilenameLinux) - }) - .then(this.authFetch(CONFIGURED_PROPAGATION_CREDENTIALS_URL, {method: 'DELETE'})); + .then(this.authFetch(CONFIGURED_PROPAGATION_CREDENTIALS_URL, {method: 'DELETE'})) ; }; - removePBAfile(apiEndpoint, setFilenameFnc) { - this.sendPbaRemoveRequest(apiEndpoint) - setFilenameFnc('') - } - sendPbaRemoveRequest(apiEndpoint) { let request_options = { method: 'DELETE', diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json deleted file mode 100644 index 42568404a..000000000 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001, - 9200 - ], - "PBA_linux_filename": "test.sh", - "PBA_windows_filename": "test.ps1", - "blocked_ips": ["192.168.1.1", "192.168.1.100"], - "custom_PBA_linux_cmd": "bash test.sh", - "custom_PBA_windows_cmd": "powershell test.ps1", - "depth": 2, - "exploit_lm_hash_list": ["lm_hash_1", "lm_hash_2"], - "exploit_ntlm_hash_list": ["nt_hash_1", "nt_hash_2", "nt_hash_3"], - "exploit_password_list": [ - "test", - "iloveyou", - "12345" - ], - "exploit_ssh_keys": [ - { - "public_key": "my_public_key", - "private_key": "my_private_key" - } - ], - "credential_collectors": ["MimikatzCollector", "SSHCollector"], - "exploit_user_list": [ - "Administrator", - "root", - "user", - "ubuntu" - ], - "exploiter_classes": [ - "SmbExploiter", - "WmiExploiter", - "SSHExploiter", - "ZerologonExploiter", - "HadoopExploiter", - "MSSQLExploiter", - "PowerShellExploiter", - "Log4ShellExploiter" - ], - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "HTTPFinger", - "MSSQLFinger", - "ElasticFinger" - ], - "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], - "keep_tunnel_open_time": 60, - "local_network_scan": true, - "ping_scan_timeout": 1000, - "post_breach_actions": [ - "CommunicateAsBackdoorUser", - "ModifyShellStartupFiles", - "ScheduleJobs", - "Timestomping", - "AccountDiscovery" - ], - "ransomware": { - "encryption": { - "enabled": true, - "directories": { - "linux_target_dir": "/tmp/ransomware-target", - "windows_target_dir": "C:\\windows\\temp\\ransomware-target" - } - }, - "other_behaviors": { - "readme": true - } - }, - "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], - "tcp_scan_timeout": 3000, - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 7001, - 8088 - ] -} diff --git a/monkey/tests/unit_tests/conftest.py b/monkey/tests/unit_tests/conftest.py index 51528ba00..7c987fe95 100644 --- a/monkey/tests/unit_tests/conftest.py +++ b/monkey/tests/unit_tests/conftest.py @@ -1,7 +1,5 @@ -import json import sys from pathlib import Path -from typing import Callable, Dict import pytest from _pytest.monkeypatch import MonkeyPatch @@ -49,15 +47,6 @@ def monkey_configs_dir(data_for_tests_dir) -> Path: return data_for_tests_dir / "monkey_configs" -@pytest.fixture -def load_monkey_config(data_for_tests_dir) -> Callable[[str], Dict]: - def inner(filename: str) -> Dict: - config_path = data_for_tests_dir / "monkey_configs" / filename - return json.loads(open(config_path, "r").read()) - - return inner - - @pytest.fixture def default_agent_configuration() -> AgentConfiguration: return DEFAULT_AGENT_CONFIGURATION diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index 52e73dc50..4eef545d5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -4,11 +4,6 @@ from tests.unit_tests.monkey_island.cc.mongomock_fixtures import * # noqa: F401 from monkey_island.cc.server_utils.encryption import unlock_datastore_encryptor -@pytest.fixture -def flat_monkey_config(load_monkey_config): - return load_monkey_config("flat_config.json") - - @pytest.fixture def uses_encryptor(data_for_tests_dir): secret = "m0nk3y_u53r:3cr3t_p455w0rd" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_post_breach_files.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_post_breach_files.py deleted file mode 100644 index 708fdc034..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_post_breach_files.py +++ /dev/null @@ -1,43 +0,0 @@ -import io -import os - -import pytest -from tests.utils import raise_ - -from monkey_island.cc.repository import LocalStorageFileRepository -from monkey_island.cc.services.post_breach_files import PostBreachFilesService - - -@pytest.fixture -def local_storage_file_repository(tmp_path): - return LocalStorageFileRepository(tmp_path) - - -@pytest.fixture(autouse=True) -def post_breach_files_service(local_storage_file_repository): - PostBreachFilesService.initialize(local_storage_file_repository) - - -def test_remove_pba_files(local_storage_file_repository, tmp_path): - local_storage_file_repository.save_file("linux_file", io.BytesIO(b"")) - local_storage_file_repository.save_file("windows_file", io.BytesIO(b"")) - assert not dir_is_empty(tmp_path) - - PostBreachFilesService.remove_PBA_files() - - assert dir_is_empty(tmp_path) - - -def dir_is_empty(dir_path): - dir_contents = os.listdir(dir_path) - return len(dir_contents) == 0 - - -def test_remove_failure(local_storage_file_repository, monkeypatch): - monkeypatch.setattr(os, "remove", lambda x: raise_(OSError("Permission denied"))) - - try: - local_storage_file_repository.save_file("windows_file", io.BytesIO(b"")) - PostBreachFilesService.remove_PBA_files() - except Exception as ex: - pytest.fail(f"Unxepected exception: {ex}") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_repository_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_repository_service.py new file mode 100644 index 000000000..adc07eb2e --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_repository_service.py @@ -0,0 +1,57 @@ +from dataclasses import replace +from unittest.mock import MagicMock + +import pytest +from tests.monkey_island import InMemoryAgentConfigurationRepository + +from common.configuration import AgentConfiguration +from monkey_island.cc.repository import IAgentConfigurationRepository, IFileRepository +from monkey_island.cc.services import RepositoryService + +LINUX_FILENAME = "linux_pba_file.sh" +WINDOWS_FILENAME = "windows_pba_file.ps1" + + +@pytest.fixture +def agent_configuration(default_agent_configuration) -> AgentConfiguration: + custom_pbas = replace( + default_agent_configuration.custom_pbas, + linux_filename=LINUX_FILENAME, + windows_filename=WINDOWS_FILENAME, + ) + return replace(default_agent_configuration, custom_pbas=custom_pbas) + + +@pytest.fixture +def agent_configuration_repository(agent_configuration) -> IAgentConfigurationRepository: + agent_configuration_repository = InMemoryAgentConfigurationRepository() + agent_configuration_repository.store_configuration(agent_configuration) + + return agent_configuration_repository + + +@pytest.fixture +def mock_file_repository(): + return MagicMock(spec=IFileRepository) + + +def test_reset_configuration__remove_pba_files( + agent_configuration_repository, mock_file_repository +): + repository_service = RepositoryService(agent_configuration_repository, mock_file_repository) + + repository_service.reset_agent_configuration() + + assert mock_file_repository.delete_file.called_with(LINUX_FILENAME) + assert mock_file_repository.delete_file.called_with(WINDOWS_FILENAME) + + +def test_reset_configuration__agent_configuration_changed( + agent_configuration_repository, agent_configuration, mock_file_repository +): + mock_file_repository = MagicMock(spec=IFileRepository) + repository_service = RepositoryService(agent_configuration_repository, mock_file_repository) + + repository_service.reset_agent_configuration() + + assert agent_configuration_repository.get_configuration() != agent_configuration diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_database_initializer.py b/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_database_initializer.py index 4afdbc969..c285c1448 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_database_initializer.py +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_database_initializer.py @@ -19,9 +19,6 @@ def patch_attack_mitigations_path(monkeypatch, data_for_tests_dir): @pytest.fixture(scope="module", autouse=True) def patch_dependencies(monkeypatch_session): - monkeypatch_session.setattr( - "monkey_island.cc.services.config.ConfigService.init_config", lambda: None - ) monkeypatch_session.setattr( "monkey_island.cc.services.database.jsonify", MagicMock(return_value=True) )