diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0dd381f..b85ed376f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - "+dev" from version numbers. #1553 - agent's "--config" argument. #906 - Option to export monkey telemetries. #1998 +- "/api/configuration/import" endpoint. #2002 +- "/api/configuration/export" endpoint. #2002 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index e2253b36c..282f93346 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -23,8 +23,6 @@ from monkey_island.cc.resources.blackbox.monkey_blackbox_endpoint import MonkeyB from monkey_island.cc.resources.blackbox.telemetry_blackbox_endpoint import ( TelemetryBlackboxEndpoint, ) -from monkey_island.cc.resources.configuration_export import ConfigurationExport -from monkey_island.cc.resources.configuration_import import ConfigurationImport from monkey_island.cc.resources.edge import Edge from monkey_island.cc.resources.exploitations.manual_exploitation import ManualExploitation from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation @@ -154,8 +152,6 @@ def init_api_resources(api: FlaskDIWrapper): api.add_resource(IslandMode) api.add_resource(IslandConfiguration) - api.add_resource(ConfigurationExport) - api.add_resource(ConfigurationImport) api.add_resource(AgentConfiguration) api.add_resource(AgentBinaries) api.add_resource(NetMap) diff --git a/monkey/monkey_island/cc/resources/configuration_export.py b/monkey/monkey_island/cc/resources/configuration_export.py deleted file mode 100644 index e10e5d1a3..000000000 --- a/monkey/monkey_island/cc/resources/configuration_export.py +++ /dev/null @@ -1,29 +0,0 @@ -import json - -from flask import request - -from monkey_island.cc.resources.AbstractResource import AbstractResource -from monkey_island.cc.resources.request_authentication import jwt_required -from monkey_island.cc.server_utils.encryption import PasswordBasedStringEncryptor -from monkey_island.cc.services.config import ConfigService - - -class ConfigurationExport(AbstractResource): - urls = ["/api/configuration/export"] - - @jwt_required - def post(self): - data = json.loads(request.data) - should_encrypt = data["should_encrypt"] - - plaintext_config = ConfigService.get_config() - - config_export = plaintext_config - if should_encrypt: - password = data["password"] - plaintext_config = json.dumps(plaintext_config) - - pb_encryptor = PasswordBasedStringEncryptor(password) - config_export = pb_encryptor.encrypt(plaintext_config) - - return {"config_export": config_export, "encrypted": should_encrypt} diff --git a/monkey/monkey_island/cc/resources/configuration_import.py b/monkey/monkey_island/cc/resources/configuration_import.py deleted file mode 100644 index 15ab3e41f..000000000 --- a/monkey/monkey_island/cc/resources/configuration_import.py +++ /dev/null @@ -1,103 +0,0 @@ -import json -import logging -from dataclasses import dataclass -from json.decoder import JSONDecodeError - -from flask import request - -from common.utils.exceptions import InvalidConfigurationError -from monkey_island.cc.resources.AbstractResource import AbstractResource -from monkey_island.cc.resources.request_authentication import jwt_required -from monkey_island.cc.server_utils.encryption import ( - InvalidCiphertextError, - InvalidCredentialsError, - PasswordBasedStringEncryptor, - is_encrypted, -) -from monkey_island.cc.services.config import ConfigService - -logger = logging.getLogger(__name__) - - -class ImportStatuses: - UNSAFE_OPTION_VERIFICATION_REQUIRED = "unsafe_options_verification_required" - INVALID_CONFIGURATION = "invalid_configuration" - INVALID_CREDENTIALS = "invalid_credentials" - IMPORTED = "imported" - - -@dataclass -class ResponseContents: - import_status: str = ImportStatuses.IMPORTED - message: str = "" - status_code: int = 200 - config: str = "" - config_schema: str = "" - - def form_response(self): - return self.__dict__ - - -class ConfigurationImport(AbstractResource): - # API Spec: Should probably be merged with IslandConfiguration - urls = ["/api/configuration/import"] - SUCCESS = False - - @jwt_required - def post(self): - request_contents = json.loads(request.data) - try: - config = ConfigurationImport._get_plaintext_config_from_request(request_contents) - if request_contents["unsafeOptionsVerified"]: - ConfigurationImport.import_config(config) - return ResponseContents().form_response() - else: - return ResponseContents( - config=json.dumps(config), - config_schema=ConfigService.get_config_schema(), - import_status=ImportStatuses.UNSAFE_OPTION_VERIFICATION_REQUIRED, - ).form_response() - # API Spec: HTTP status code should be 401 here - except InvalidCredentialsError: - return ResponseContents( - import_status=ImportStatuses.INVALID_CREDENTIALS, - message="Invalid credentials provided", - ).form_response() - # API Spec: HTTP status code should be 400 (or something else) here - except InvalidConfigurationError: - return ResponseContents( - import_status=ImportStatuses.INVALID_CONFIGURATION, - message="Invalid configuration supplied. " - "Maybe the format is outdated or the file has been corrupted.", - ).form_response() - - @staticmethod - def _get_plaintext_config_from_request(request_contents: dict) -> dict: - try: - config = request_contents["config"] - if ConfigurationImport.is_config_encrypted(request_contents["config"]): - pb_encryptor = PasswordBasedStringEncryptor(request_contents["password"]) - config = pb_encryptor.decrypt(config) - return json.loads(config) - except (JSONDecodeError, InvalidCiphertextError): - logger.exception( - "Exception encountered when trying to extract plaintext configuration." - ) - raise InvalidConfigurationError - - @staticmethod - def import_config(config_json): - if not ConfigService.update_config(config_json, should_encrypt=True): - raise InvalidConfigurationError - - @staticmethod - def is_config_encrypted(config: str): - try: - if config.startswith("{"): - return False - elif is_encrypted(config): - return True - else: - raise InvalidConfigurationError - except Exception: - raise InvalidConfigurationError diff --git a/monkey/monkey_island/cc/server_utils/encryption/__init__.py b/monkey/monkey_island/cc/server_utils/encryption/__init__.py index 7fa6c77a4..a6671afb6 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/__init__.py +++ b/monkey/monkey_island/cc/server_utils/encryption/__init__.py @@ -2,10 +2,6 @@ from .i_encryptor import IEncryptor from .key_based_encryptor import ( KeyBasedEncryptor, ) -from .password_based_string_encryptor import ( - PasswordBasedStringEncryptor, - is_encrypted, -) from .password_based_bytes_encryptor import ( PasswordBasedBytesEncryptor, InvalidCredentialsError, diff --git a/monkey/monkey_island/cc/server_utils/encryption/password_based_string_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/password_based_string_encryptor.py deleted file mode 100644 index dac7276e9..000000000 --- a/monkey/monkey_island/cc/server_utils/encryption/password_based_string_encryptor.py +++ /dev/null @@ -1,33 +0,0 @@ -import base64 -import logging - -import pyAesCrypt - -from .i_encryptor import IEncryptor -from .password_based_bytes_encryptor import PasswordBasedBytesEncryptor - -logger = logging.getLogger(__name__) - - -class PasswordBasedStringEncryptor(IEncryptor): - - _BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef - - def __init__(self, password: str): - self.password = password - - def encrypt(self, plaintext: str) -> str: - ciphertext = PasswordBasedBytesEncryptor(self.password).encrypt(plaintext.encode()) - - return base64.b64encode(ciphertext).decode() - - def decrypt(self, ciphertext: str) -> str: - ciphertext = base64.b64decode(ciphertext) - - plaintext_stream = PasswordBasedBytesEncryptor(self.password).decrypt(ciphertext) - return plaintext_stream.decode() - - -def is_encrypted(ciphertext: str) -> bool: - ciphertext = base64.b64decode(ciphertext) - return ciphertext.startswith(b"AES") diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx index d30438cbd..9dc785f73 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx @@ -12,6 +12,7 @@ type Props = { } const ConfigExportModal = (props: Props) => { + // TODO: Change this endpoint to new agent-configuration endpoint const configExportEndpoint = '/api/configuration/export'; const [pass, setPass] = useState(''); diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx index 8a600ab28..70e366d20 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx @@ -18,6 +18,7 @@ type Props = { const ConfigImportModal = (props: Props) => { + // TODO: change this endpoint to the new configuration import endpoint const configImportEndpoint = '/api/configuration/import'; const [uploadStatus, setUploadStatus] = useState(UploadStatuses.clean); diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json deleted file mode 100644 index 678023ebb..000000000 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "basic": { - "exploiters": { - "exploiter_classes": [ - "SmbExploiter", - "WmiExploiter", - "SSHExploiter", - "HadoopExploiter", - "MSSQLExploiter" - ] - }, - "credentials": { - "exploit_user_list": [ - "Administrator", - "root", - "user" - ], - "exploit_password_list": [ - "root", - "123456", - "password", - "123456789", - "qwerty", - "111111", - "iloveyou" - ] - } - }, - "basic_network": { - "scope": { - "blocked_ips": [], - "local_network_scan": true, - "depth": 2, - "subnet_scan_list": [] - }, - "network_analysis": { - "inaccessible_subnets": [] - } - }, - "internal": { - "general": { - "keep_tunnel_open_time": 60 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001, - 9200 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 7001, - 8088 - ], - "tcp_scan_timeout": 3000 - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "HTTPFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [] - } - }, - "monkey": { - "post_breach": { - "custom_pba_linux_cmd": "", - "custom_pba_windows_cmd": "", - "pba_windows_filename": "", - "pba_linux_filename": "", - "post_breach_actions": [ - "communicateasbackdooruser", - "modifyshellstartupfiles", - "hiddenfiles", - "trapcommand", - "changesetuidsetgid", - "schedulejobs", - "timestomping", - "accountdiscovery" - ] - }, - "system_info": { - "system_info_collector_classes": [ - "MimikatzCollector", - "SSHCollector" - ] - } - } - } diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index ba5a2c66e..52e73dc50 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -1,28 +1,14 @@ -# Without these imports pytests can't use fixtures, -# because they are not found -import json - import pytest from tests.unit_tests.monkey_island.cc.mongomock_fixtures import * # noqa: F401,F403,E402 from monkey_island.cc.server_utils.encryption import unlock_datastore_encryptor -@pytest.fixture -def monkey_config(load_monkey_config): - return load_monkey_config("monkey_config_standard.json") - - @pytest.fixture def flat_monkey_config(load_monkey_config): return load_monkey_config("flat_config.json") -@pytest.fixture -def monkey_config_json(monkey_config): - return json.dumps(monkey_config) - - @pytest.fixture def uses_encryptor(data_for_tests_dir): secret = "m0nk3y_u53r:3cr3t_p455w0rd" diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py deleted file mode 100644 index bf7ccff80..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest -from tests.unit_tests.monkey_island.cc.server_utils.encryption.test_password_based_encryption import ( # noqa: E501 - PASSWORD, -) -from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption_test import ( - MALFORMED_CIPHER_TEXT_CORRUPTED, -) - -from common.utils.exceptions import InvalidConfigurationError -from monkey_island.cc.resources.configuration_import import ConfigurationImport -from monkey_island.cc.server_utils.encryption import PasswordBasedStringEncryptor - - -def test_is_config_encrypted__json(monkey_config_json): - assert not ConfigurationImport.is_config_encrypted(monkey_config_json) - - -@pytest.mark.slow -def test_is_config_encrypted__ciphertext(monkey_config_json): - pb_encryptor = PasswordBasedStringEncryptor(PASSWORD) - encrypted_config = pb_encryptor.encrypt(monkey_config_json) - assert ConfigurationImport.is_config_encrypted(encrypted_config) - - -def test_is_config_encrypted__corrupt_ciphertext(): - with pytest.raises(InvalidConfigurationError): - assert ConfigurationImport.is_config_encrypted(MALFORMED_CIPHER_TEXT_CORRUPTED) - - -def test_is_config_encrypted__unknown_format(): - with pytest.raises(InvalidConfigurationError): - assert ConfigurationImport.is_config_encrypted("ABC") diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py deleted file mode 100644 index 038b17ec1..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest -from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption_test import ( - MALFORMED_CIPHER_TEXT_CORRUPTED, - VALID_CIPHER_TEXT, -) - -from monkey_island.cc.server_utils.encryption import ( - InvalidCiphertextError, - InvalidCredentialsError, - PasswordBasedStringEncryptor, -) - -# Mark all tests in this module as slow -pytestmark = pytest.mark.slow - -PASSWORD = "hello123" -INCORRECT_PASSWORD = "goodbye321" - - -def test_encrypt_decrypt_string(monkey_config_json): - pb_encryptor = PasswordBasedStringEncryptor(PASSWORD) - encrypted_config = pb_encryptor.encrypt(monkey_config_json) - assert pb_encryptor.decrypt(encrypted_config) == monkey_config_json - - -def test_decrypt_string__wrong_password(monkey_config_json): - pb_encryptor = PasswordBasedStringEncryptor(INCORRECT_PASSWORD) - with pytest.raises(InvalidCredentialsError): - pb_encryptor.decrypt(VALID_CIPHER_TEXT) - - -def test_decrypt_string__malformed_corrupted(): - pb_encryptor = PasswordBasedStringEncryptor(PASSWORD) - with pytest.raises(ValueError): - pb_encryptor.decrypt(MALFORMED_CIPHER_TEXT_CORRUPTED) - - -def test_decrypt_string__no_password(monkey_config_json): - pb_encryptor = PasswordBasedStringEncryptor("") - with pytest.raises(InvalidCredentialsError): - pb_encryptor.decrypt(VALID_CIPHER_TEXT) - - -def test_decrypt_string__invalid_cyphertext(monkey_config_json): - pb_encryptor = PasswordBasedStringEncryptor("") - with pytest.raises(InvalidCiphertextError): - pb_encryptor.decrypt("")