From 7a28ba4c4d818998973a7339b041273de7ecd404 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 20 Jun 2022 15:25:36 -0700 Subject: [PATCH 01/29] Island: Create new resource `Configuration` --- .../cc/resources/configuration.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 monkey/monkey_island/cc/resources/configuration.py diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py new file mode 100644 index 000000000..79f1d2a0c --- /dev/null +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -0,0 +1,18 @@ +from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.resources.request_authentication import jwt_required + + +class Configuration(AbstractResource): + urls = ["/api/configuration"] + + @jwt_required + def get(self): + pass + + @jwt_required + def post(self): + pass + + @jwt_required + def patch(self): # reset the config here? + pass From 5dd27eeea535e6250f241a8fb3d22816124630d4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 20 Jun 2022 19:50:38 -0700 Subject: [PATCH 02/29] Island: Add definition for GET in new Configuration resource --- .../cc/resources/configuration.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 79f1d2a0c..055fe5db1 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -1,13 +1,26 @@ +from enum import Enum +from flask import jsonify, request + from monkey_island.cc.resources.AbstractResource import AbstractResource from monkey_island.cc.resources.request_authentication import jwt_required +from monkey_island.cc.repository import FileAgentConfigurationRepository + +class ConfigurationTypeEnum(Enum): + ISLAND = "island" + AGENT = "agent" class Configuration(AbstractResource): - urls = ["/api/configuration"] + urls = ["/api/configuration/"] @jwt_required - def get(self): - pass + def get(self, configuration_type: str): + # we probably still need this because of credential fields, HTTP ports, etc in the config? + if configuration_type == ConfigurationTypeEnum.ISLAND: + pass + elif configuration_type == ConfigurationTypeEnum.AGENT: + configuration = FileAgentConfigurationRepository.get_configuration() + return jsonify(configuration=configuration) @jwt_required def post(self): From 138480f0217a66438c68944c1efddaab8e1cc72e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 20 Jun 2022 23:01:12 -0700 Subject: [PATCH 03/29] Island: Roughly implement POST in new Configuration resource + lots of questions --- .../cc/resources/configuration.py | 77 +++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 055fe5db1..dfd7ae70c 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -1,21 +1,49 @@ +import json +from dataclasses import dataclass from enum import Enum +from itertools import chain +from typing import Mapping + from flask import jsonify, request +from common.configuration.agent_configuration import AgentConfigurationSchema +from common.utils.exceptions import InvalidConfigurationError +from monkey_island.cc.repository import FileAgentConfigurationRepository from monkey_island.cc.resources.AbstractResource import AbstractResource from monkey_island.cc.resources.request_authentication import jwt_required -from monkey_island.cc.repository import FileAgentConfigurationRepository + class ConfigurationTypeEnum(Enum): ISLAND = "island" AGENT = "agent" +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 Configuration(AbstractResource): urls = ["/api/configuration/"] @jwt_required def get(self, configuration_type: str): - # we probably still need this because of credential fields, HTTP ports, etc in the config? + # Q: we probably still need this because of credential fields, HTTP ports, etc in the + # config? if configuration_type == ConfigurationTypeEnum.ISLAND: pass elif configuration_type == ConfigurationTypeEnum.AGENT: @@ -23,9 +51,48 @@ class Configuration(AbstractResource): return jsonify(configuration=configuration) @jwt_required - def post(self): - pass + def post(self, configuration_type: str): + request_contents = json.loads(request.data) + configuration_json = json.loads(request_contents["config"]) + Configuration._remove_metadata_from_config(configuration_json) + + try: + # Q: encryption is moving to the frontend; also check this in the frontend? + if request_contents["unsafeOptionsVerified"]: + schema = AgentConfigurationSchema() + # Q: in what format/schema are we getting the config from the Island? + # Q: when does flattening the config go away? + configuration_object = schema.loads(configuration_json) + FileAgentConfigurationRepository.store_configuration( + configuration_object + ) # check error handling + return ResponseContents().form_response() + else: + return ResponseContents( + config=json.dumps(configuration_json), + # Q: do we still need a separate config schema like this? + # config_schema=ConfigService.get_config_schema(), + import_status=ImportStatuses.UNSAFE_OPTION_VERIFICATION_REQUIRED, + ).form_response() + except InvalidConfigurationError: + return ResponseContents( + import_status=ImportStatuses.INVALID_CONFIGURATION, + message="Invalid configuration supplied. " + "Maybe the format is outdated or the file has been corrupted.", + status_code=400, + ).form_response() + + @staticmethod + # Q: why is this really needed? besides the fact that it just doesn't belong in the config + # which is being saved in mongo? if nothing, can't we just wait to change the exploiters + # to plugins? + def _remove_metadata_from_config(configuration_json: Mapping): + for exploiter in chain( + configuration_json["propagation"]["exploitation"]["brute_force"], + configuration_json["propagation"]["exploitation"]["vulnerability"], + ): + del exploiter["supported_os"] @jwt_required - def patch(self): # reset the config here? + def patch(self): # Q: reset the configuration here? or does that make more sense in delete? pass From 5f253e79b306c81207f5a8ac4ab289eb41c6d11a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 21 Jun 2022 12:50:52 +0200 Subject: [PATCH 04/29] Island: Init Configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index dfd7ae70c..417db6832 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -8,7 +8,7 @@ from flask import jsonify, request from common.configuration.agent_configuration import AgentConfigurationSchema from common.utils.exceptions import InvalidConfigurationError -from monkey_island.cc.repository import FileAgentConfigurationRepository +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 @@ -40,6 +40,9 @@ class ResponseContents: class Configuration(AbstractResource): urls = ["/api/configuration/"] + def __init__(self, file_agent_configuration_repository: IAgentConfigurationRepository): + self._file_agent_configuration_repository = file_agent_configuration_repository + @jwt_required def get(self, configuration_type: str): # Q: we probably still need this because of credential fields, HTTP ports, etc in the @@ -47,7 +50,7 @@ class Configuration(AbstractResource): if configuration_type == ConfigurationTypeEnum.ISLAND: pass elif configuration_type == ConfigurationTypeEnum.AGENT: - configuration = FileAgentConfigurationRepository.get_configuration() + configuration = self._file_agent_configuration_repository.get_configuration() return jsonify(configuration=configuration) @jwt_required @@ -63,7 +66,7 @@ class Configuration(AbstractResource): # Q: in what format/schema are we getting the config from the Island? # Q: when does flattening the config go away? configuration_object = schema.loads(configuration_json) - FileAgentConfigurationRepository.store_configuration( + self._file_agent_configuration_repository.store_configuration( configuration_object ) # check error handling return ResponseContents().form_response() From 74bc55e077132862f4995ac20f7736eed3664e5a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 14:31:23 -0700 Subject: [PATCH 05/29] Island: Remove config type logic from new configuration resource --- .../cc/resources/configuration.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 417db6832..b611eaec6 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -1,6 +1,5 @@ import json from dataclasses import dataclass -from enum import Enum from itertools import chain from typing import Mapping @@ -13,11 +12,6 @@ from monkey_island.cc.resources.AbstractResource import AbstractResource from monkey_island.cc.resources.request_authentication import jwt_required -class ConfigurationTypeEnum(Enum): - ISLAND = "island" - AGENT = "agent" - - class ImportStatuses: UNSAFE_OPTION_VERIFICATION_REQUIRED = "unsafe_options_verification_required" INVALID_CONFIGURATION = "invalid_configuration" @@ -38,23 +32,18 @@ class ResponseContents: class Configuration(AbstractResource): - urls = ["/api/configuration/"] + urls = ["/api/configuration"] def __init__(self, file_agent_configuration_repository: IAgentConfigurationRepository): self._file_agent_configuration_repository = file_agent_configuration_repository @jwt_required - def get(self, configuration_type: str): - # Q: we probably still need this because of credential fields, HTTP ports, etc in the - # config? - if configuration_type == ConfigurationTypeEnum.ISLAND: - pass - elif configuration_type == ConfigurationTypeEnum.AGENT: - configuration = self._file_agent_configuration_repository.get_configuration() - return jsonify(configuration=configuration) + def get(self): + configuration = self._file_agent_configuration_repository.get_configuration() + return jsonify(configuration=configuration) @jwt_required - def post(self, configuration_type: str): + def post(self): request_contents = json.loads(request.data) configuration_json = json.loads(request_contents["config"]) Configuration._remove_metadata_from_config(configuration_json) From 0d8cc713d2502601f1173bbe85d27f51c71cf94b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 14:35:42 -0700 Subject: [PATCH 06/29] Island: Rename `file_agent_configuration_repository` to `agent_configuration_repository` in new configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index b611eaec6..0eab79bf9 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -34,12 +34,12 @@ class ResponseContents: class Configuration(AbstractResource): urls = ["/api/configuration"] - def __init__(self, file_agent_configuration_repository: IAgentConfigurationRepository): - self._file_agent_configuration_repository = file_agent_configuration_repository + def __init__(self, agent_configuration_repository: IAgentConfigurationRepository): + self._agent_configuration_repository = agent_configuration_repository @jwt_required def get(self): - configuration = self._file_agent_configuration_repository.get_configuration() + configuration = self._agent_configuration_repository.get_configuration() return jsonify(configuration=configuration) @jwt_required @@ -55,7 +55,7 @@ class Configuration(AbstractResource): # Q: in what format/schema are we getting the config from the Island? # Q: when does flattening the config go away? configuration_object = schema.loads(configuration_json) - self._file_agent_configuration_repository.store_configuration( + self._agent_configuration_repository.store_configuration( configuration_object ) # check error handling return ResponseContents().form_response() From 8c14423c4ea5efb7c81a7e1ee270f34c70fbeaa8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 14:41:55 -0700 Subject: [PATCH 07/29] Island: Prepend 'agent' to everything having 'configuration' in new configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 0eab79bf9..710db13aa 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -31,8 +31,8 @@ class ResponseContents: return self.__dict__ -class Configuration(AbstractResource): - urls = ["/api/configuration"] +class AgentConfiguration(AbstractResource): + urls = ["/api/agent-configuration"] def __init__(self, agent_configuration_repository: IAgentConfigurationRepository): self._agent_configuration_repository = agent_configuration_repository @@ -46,7 +46,7 @@ class Configuration(AbstractResource): def post(self): request_contents = json.loads(request.data) configuration_json = json.loads(request_contents["config"]) - Configuration._remove_metadata_from_config(configuration_json) + AgentConfiguration._remove_metadata_from_config(configuration_json) try: # Q: encryption is moving to the frontend; also check this in the frontend? From 15615e08c47e619a8d2b0adab0adcb7711fb227d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 14:51:40 -0700 Subject: [PATCH 08/29] Island: Get rid of unsafe config options' check in new configuration resource --- .../cc/resources/configuration.py | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 710db13aa..24185eee9 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -49,24 +49,13 @@ class AgentConfiguration(AbstractResource): AgentConfiguration._remove_metadata_from_config(configuration_json) try: - # Q: encryption is moving to the frontend; also check this in the frontend? - if request_contents["unsafeOptionsVerified"]: - schema = AgentConfigurationSchema() - # Q: in what format/schema are we getting the config from the Island? - # Q: when does flattening the config go away? - configuration_object = schema.loads(configuration_json) - self._agent_configuration_repository.store_configuration( - configuration_object - ) # check error handling - return ResponseContents().form_response() - else: - return ResponseContents( - config=json.dumps(configuration_json), - # Q: do we still need a separate config schema like this? - # config_schema=ConfigService.get_config_schema(), - import_status=ImportStatuses.UNSAFE_OPTION_VERIFICATION_REQUIRED, - ).form_response() - except InvalidConfigurationError: + schema = AgentConfigurationSchema() + configuration_object = schema.loads(configuration_json) + self._agent_configuration_repository.store_configuration( + configuration_object + ) # check error handling + return ResponseContents().form_response() + except InvalidConfigurationError: # don't need this probably either; if invalid, schema should raise error (catch marshmallow exception and return 400) return ResponseContents( import_status=ImportStatuses.INVALID_CONFIGURATION, message="Invalid configuration supplied. " From 00e38391b48175d35b1c88a86b0e45a9b60898cf Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 14:58:08 -0700 Subject: [PATCH 09/29] Island: Catch appropriate `marshmallow` error when loading config which could be invalid, in new configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 24185eee9..84c28e337 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -3,19 +3,17 @@ from dataclasses import dataclass from itertools import chain from typing import Mapping +import marshmallow from flask import jsonify, request from common.configuration.agent_configuration import AgentConfigurationSchema -from common.utils.exceptions import InvalidConfigurationError 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 ImportStatuses: - UNSAFE_OPTION_VERIFICATION_REQUIRED = "unsafe_options_verification_required" INVALID_CONFIGURATION = "invalid_configuration" - INVALID_CREDENTIALS = "invalid_credentials" IMPORTED = "imported" @@ -51,11 +49,9 @@ class AgentConfiguration(AbstractResource): try: schema = AgentConfigurationSchema() configuration_object = schema.loads(configuration_json) - self._agent_configuration_repository.store_configuration( - configuration_object - ) # check error handling + self._agent_configuration_repository.store_configuration(configuration_object) return ResponseContents().form_response() - except InvalidConfigurationError: # don't need this probably either; if invalid, schema should raise error (catch marshmallow exception and return 400) + except marshmallow.exceptions.ValidationError: return ResponseContents( import_status=ImportStatuses.INVALID_CONFIGURATION, message="Invalid configuration supplied. " From 63d5330386b1340cc9bc347955cbd233b341ab84 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 15:07:32 -0700 Subject: [PATCH 10/29] Island: Remove unneeded patch function in new configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 84c28e337..ae9b86688 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -69,7 +69,3 @@ class AgentConfiguration(AbstractResource): configuration_json["propagation"]["exploitation"]["vulnerability"], ): del exploiter["supported_os"] - - @jwt_required - def patch(self): # Q: reset the configuration here? or does that make more sense in delete? - pass From 32fe7c6a4b6ebfaf0e44aa471eab2582c9414208 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 15:09:16 -0700 Subject: [PATCH 11/29] Island: Remove unneeded fields from `ResponseContents` in new configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index ae9b86688..2ec3c095a 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -22,8 +22,6 @@ 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__ From 6b45d62d813066ab690b954b439f9e38392284cb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 15:31:38 -0700 Subject: [PATCH 12/29] Island: Fix logic to remove metadata from config in new configuration resource's POST --- monkey/monkey_island/cc/resources/configuration.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 2ec3c095a..2ed567561 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -58,12 +58,10 @@ class AgentConfiguration(AbstractResource): ).form_response() @staticmethod - # Q: why is this really needed? besides the fact that it just doesn't belong in the config - # which is being saved in mongo? if nothing, can't we just wait to change the exploiters - # to plugins? def _remove_metadata_from_config(configuration_json: Mapping): for exploiter in chain( configuration_json["propagation"]["exploitation"]["brute_force"], configuration_json["propagation"]["exploitation"]["vulnerability"], ): - del exploiter["supported_os"] + if "supported_os" in exploiter: + del exploiter["supported_os"] From 922495785c19f3624a15a608f8fb3ddf13aa0fc0 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 15:55:39 -0700 Subject: [PATCH 13/29] Island: Create class variable for agent config schema in new configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 2ed567561..5ec163ded 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -32,6 +32,7 @@ class AgentConfiguration(AbstractResource): def __init__(self, agent_configuration_repository: IAgentConfigurationRepository): self._agent_configuration_repository = agent_configuration_repository + self._schema = AgentConfigurationSchema() @jwt_required def get(self): @@ -45,8 +46,7 @@ class AgentConfiguration(AbstractResource): AgentConfiguration._remove_metadata_from_config(configuration_json) try: - schema = AgentConfigurationSchema() - configuration_object = schema.loads(configuration_json) + configuration_object = self._schema.loads(configuration_json) self._agent_configuration_repository.store_configuration(configuration_object) return ResponseContents().form_response() except marshmallow.exceptions.ValidationError: From d861def86cfea2860c6770e27feea6c11e6822e8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Jun 2022 15:57:58 -0700 Subject: [PATCH 14/29] Island: Add logic to add metadata to config in new configuration resource's GET --- .../monkey_island/cc/resources/configuration.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 5ec163ded..7b8929ab9 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -7,6 +7,7 @@ import marshmallow from flask import jsonify, request from common.configuration.agent_configuration import AgentConfigurationSchema +from common.configuration.default_agent_configuration import DEFAULT_AGENT_CONFIGURATION 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 @@ -37,7 +38,21 @@ class AgentConfiguration(AbstractResource): @jwt_required def get(self): configuration = self._agent_configuration_repository.get_configuration() - return jsonify(configuration=configuration) + configuration_json = self._schema.dumps(configuration) + # for when the agent requests the config; it needs the exploiters' metadata + # Q: but this'll cause issues when the frontend requests the config? + AgentConfiguration._add_metadata_to_config(configuration_json) + return jsonify(configuration_json=configuration_json) + + @staticmethod + def _add_metadata_to_config(configuration_json): + for exploiter_type in ["brute_force", "vulnerability"]: + for exploiter in configuration_json["propagation"]["exploitation"][exploiter_type]: + # there has to be a better way to do this + if "supported_os" not in exploiter: + exploiter["supported_os"] = DEFAULT_AGENT_CONFIGURATION["propagation"][ + "exploitation" + ][exploiter_type]["supported_os"] @jwt_required def post(self): From ec710d9e5f4b7f72b0252e5f8f9579c9243f3153 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 07:48:34 -0700 Subject: [PATCH 15/29] Island: Get rid of ResponseContents and ImportStatuses in new configuration resource --- .../cc/resources/configuration.py | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 7b8929ab9..725dbe580 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -1,10 +1,9 @@ import json -from dataclasses import dataclass from itertools import chain from typing import Mapping import marshmallow -from flask import jsonify, request +from flask import jsonify, make_response, request from common.configuration.agent_configuration import AgentConfigurationSchema from common.configuration.default_agent_configuration import DEFAULT_AGENT_CONFIGURATION @@ -13,21 +12,6 @@ from monkey_island.cc.resources.AbstractResource import AbstractResource from monkey_island.cc.resources.request_authentication import jwt_required -class ImportStatuses: - INVALID_CONFIGURATION = "invalid_configuration" - IMPORTED = "imported" - - -@dataclass -class ResponseContents: - import_status: str = ImportStatuses.IMPORTED - message: str = "" - status_code: int = 200 - - def form_response(self): - return self.__dict__ - - class AgentConfiguration(AbstractResource): urls = ["/api/agent-configuration"] @@ -63,14 +47,15 @@ class AgentConfiguration(AbstractResource): try: configuration_object = self._schema.loads(configuration_json) self._agent_configuration_repository.store_configuration(configuration_object) - return ResponseContents().form_response() + return make_response({}, 200) except marshmallow.exceptions.ValidationError: - return ResponseContents( - import_status=ImportStatuses.INVALID_CONFIGURATION, - message="Invalid configuration supplied. " - "Maybe the format is outdated or the file has been corrupted.", - status_code=400, - ).form_response() + return make_response( + { + "message": "Invalid configuration supplied. " + "Maybe the format is outdated or the file has been corrupted." + }, + 400, + ) @staticmethod def _remove_metadata_from_config(configuration_json: Mapping): From 142eed72ac5a75302eaa701ec796c271a94046c9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 08:12:09 -0700 Subject: [PATCH 16/29] Island: Remove logic to remove/add config metadata in new configuration resource --- .../cc/resources/configuration.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 725dbe580..0a889a6da 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -1,12 +1,9 @@ import json -from itertools import chain -from typing import Mapping import marshmallow from flask import jsonify, make_response, request from common.configuration.agent_configuration import AgentConfigurationSchema -from common.configuration.default_agent_configuration import DEFAULT_AGENT_CONFIGURATION 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 @@ -23,26 +20,12 @@ class AgentConfiguration(AbstractResource): def get(self): configuration = self._agent_configuration_repository.get_configuration() configuration_json = self._schema.dumps(configuration) - # for when the agent requests the config; it needs the exploiters' metadata - # Q: but this'll cause issues when the frontend requests the config? - AgentConfiguration._add_metadata_to_config(configuration_json) return jsonify(configuration_json=configuration_json) - @staticmethod - def _add_metadata_to_config(configuration_json): - for exploiter_type in ["brute_force", "vulnerability"]: - for exploiter in configuration_json["propagation"]["exploitation"][exploiter_type]: - # there has to be a better way to do this - if "supported_os" not in exploiter: - exploiter["supported_os"] = DEFAULT_AGENT_CONFIGURATION["propagation"][ - "exploitation" - ][exploiter_type]["supported_os"] - @jwt_required def post(self): request_contents = json.loads(request.data) configuration_json = json.loads(request_contents["config"]) - AgentConfiguration._remove_metadata_from_config(configuration_json) try: configuration_object = self._schema.loads(configuration_json) @@ -56,12 +39,3 @@ class AgentConfiguration(AbstractResource): }, 400, ) - - @staticmethod - def _remove_metadata_from_config(configuration_json: Mapping): - for exploiter in chain( - configuration_json["propagation"]["exploitation"]["brute_force"], - configuration_json["propagation"]["exploitation"]["vulnerability"], - ): - if "supported_os" in exploiter: - del exploiter["supported_os"] From 03037b566233864906e8bbcef49fc7e8bd83dd4b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 08:28:57 -0700 Subject: [PATCH 17/29] Common: Remove `supported_os` field for exploiters in configuration --- .../agent_sub_configuration_schemas.py | 4 -- .../configuration/agent_sub_configurations.py | 3 -- .../default_agent_configuration.py | 44 ++++++------------- 3 files changed, 14 insertions(+), 37 deletions(-) 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": {} + } ] } From 26ece213a23ec8b5f09103943bf350723b6cc1b2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 08:30:44 -0700 Subject: [PATCH 18/29] Island: Remove logic to add `supported_os` for exploiters to configuration --- monkey/monkey_island/cc/services/config.py | 24 +--------------------- 1 file changed, 1 insertion(+), 23 deletions(-) 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 From fd41d9179e9c279a61f2dfa5df377f5c59fb3f20 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 08:34:55 -0700 Subject: [PATCH 19/29] Agent: Add `SUPPORTED_OS` dict for exploiters and change checking logic in master --- monkey/infection_monkey/master/exploiter.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index b9eada5b6..8f99cba7b 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.value, OperatingSystems.WINDOWS.value], + "Log4ShellExploiter": [OperatingSystems.LINUX.value, OperatingSystems.WINDOWS.value], + "MSSQLExploiter": [OperatingSystems.WINDOWS.value], + "PowerShellExploiter": [OperatingSystems.WINDOWS.value], + "SSHExploiter": [OperatingSystems.LINUX.value], + "SmbExploiter": [OperatingSystems.WINDOWS.value], + "WmiExploiter": [OperatingSystems.WINDOWS.value], + "ZerologonExploiter": [OperatingSystems.WINDOWS.value], +} + + class Exploiter: def __init__( self, @@ -118,7 +131,7 @@ 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"]: + 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})" From e25eb194a1a00b117384a931e124edc28ce446f0 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 08:41:11 -0700 Subject: [PATCH 20/29] UT: Remove `supported_os` for exploiters from all tests --- .../tests/common/example_agent_configuration.py | 4 +--- .../monkey_configs/automated_master_config.json | 16 ++++++++-------- .../common/test_agent_configuration.py | 7 +------ .../infection_monkey/master/test_exploiter.py | 8 ++++---- .../monkey_island/cc/services/test_config.py | 11 ++--------- 5 files changed, 16 insertions(+), 30 deletions(-) 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/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/master/test_exploiter.py b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py index 3c76c903f..42b64821f 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -38,12 +38,12 @@ def exploiter_config(): return { "options": {"dropper_path_linux": "/tmp/monkey"}, "brute_force": [ - {"name": "HadoopExploiter", "supported_os": ["windows"], "options": {"timeout": 10}}, - {"name": "SSHExploiter", "supported_os": ["linux"], "options": {}}, - {"name": "WmiExploiter", "supported_os": ["windows"], "options": {"timeout": 10}}, + {"name": "HadoopExploiter", "options": {"timeout": 10}}, + {"name": "SSHExploiter", "options": {}}, + {"name": "WmiExploiter", "options": {"timeout": 10}}, ], "vulnerability": [ - {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}, + {"name": "ZerologonExploiter", "options": {}}, ], } 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": {}, }, ], From 104c7ac210af96ea64f7f1c6f21f028d2e32e91d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 08:51:21 -0700 Subject: [PATCH 21/29] Island: Fix function call to load config in new configuration resource --- monkey/monkey_island/cc/resources/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/configuration.py index 0a889a6da..6e4a00826 100644 --- a/monkey/monkey_island/cc/resources/configuration.py +++ b/monkey/monkey_island/cc/resources/configuration.py @@ -28,7 +28,7 @@ class AgentConfiguration(AbstractResource): configuration_json = json.loads(request_contents["config"]) try: - configuration_object = self._schema.loads(configuration_json) + configuration_object = self._schema.load(configuration_json) self._agent_configuration_repository.store_configuration(configuration_object) return make_response({}, 200) except marshmallow.exceptions.ValidationError: From 452028f221034722bedb635d3afc80e3dae13f0b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Jun 2022 09:27:47 -0700 Subject: [PATCH 22/29] UT: Replace Hadoop with MSSQL in test data Previously, in the UT data, Hadoop had only windows in the "supported_os" field in the config. Now that that field is stripped out from the config, the supported OSes are picked up from the main code (from `SUPPORTED_OS` in the master's `Exploiter` class) which has both winodws and linux for Hadoop. This caused the tests to fail. This commit changes the UT data to include the MSSQL exploiter (windows only) instead of the Hadoop exploiter. The tests pass now. --- .../unit_tests/infection_monkey/master/test_exploiter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 42b64821f..f48dc74d4 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -38,7 +38,7 @@ def exploiter_config(): return { "options": {"dropper_path_linux": "/tmp/monkey"}, "brute_force": [ - {"name": "HadoopExploiter", "options": {"timeout": 10}}, + {"name": "MSSQLExploiter", "options": {"timeout": 10}}, {"name": "SSHExploiter", "options": {}}, {"name": "WmiExploiter", "options": {"timeout": 10}}, ], @@ -105,11 +105,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 @@ -196,6 +196,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 From 48fab89e11a20e0440a2253c1e735d10888bf810 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Jun 2022 09:50:24 +0200 Subject: [PATCH 23/29] Island: Rename configuration.py to agent_configuration.py Per convention it must match class name --- .../cc/resources/{configuration.py => agent_configuration.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/monkey_island/cc/resources/{configuration.py => agent_configuration.py} (100%) diff --git a/monkey/monkey_island/cc/resources/configuration.py b/monkey/monkey_island/cc/resources/agent_configuration.py similarity index 100% rename from monkey/monkey_island/cc/resources/configuration.py rename to monkey/monkey_island/cc/resources/agent_configuration.py From 39e4180dfe4daec53a292bf6342df2a6f43a2700 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Jun 2022 14:26:14 +0200 Subject: [PATCH 24/29] Island: Use make_response in GET agent_configuration --- .../cc/resources/agent_configuration.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/resources/agent_configuration.py b/monkey/monkey_island/cc/resources/agent_configuration.py index 6e4a00826..ed0258471 100644 --- a/monkey/monkey_island/cc/resources/agent_configuration.py +++ b/monkey/monkey_island/cc/resources/agent_configuration.py @@ -1,7 +1,7 @@ import json import marshmallow -from flask import jsonify, make_response, request +from flask import make_response, request from common.configuration.agent_configuration import AgentConfigurationSchema from monkey_island.cc.repository import IAgentConfigurationRepository @@ -20,22 +20,19 @@ class AgentConfiguration(AbstractResource): def get(self): configuration = self._agent_configuration_repository.get_configuration() configuration_json = self._schema.dumps(configuration) - return jsonify(configuration_json=configuration_json) + return make_response(configuration_json, 200) @jwt_required def post(self): - request_contents = json.loads(request.data) - configuration_json = json.loads(request_contents["config"]) try: - configuration_object = self._schema.load(configuration_json) + request_contents = json.loads(request.data) + + configuration_object = self._schema.load(request_contents) self._agent_configuration_repository.store_configuration(configuration_object) return make_response({}, 200) - except marshmallow.exceptions.ValidationError: + except (marshmallow.exceptions.ValidationError, json.JSONDecodeError) as err: return make_response( - { - "message": "Invalid configuration supplied. " - "Maybe the format is outdated or the file has been corrupted." - }, + {"message": f"Invalid configuration supplied: {err}"}, 400, ) From 891794d927210ddaf2bf56bc23a84a7e16784a65 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Jun 2022 14:27:58 +0200 Subject: [PATCH 25/29] Island: Add AgentConfiguration resource to app.py --- monkey/monkey_island/cc/app.py | 2 ++ 1 file changed, 2 insertions(+) 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) From b35832b9dd960742da320a10618d831100fdeef5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Jun 2022 14:29:30 +0200 Subject: [PATCH 26/29] UT: Add InMemoryFileAgentConfigurationRepository --- monkey/tests/monkey_island/__init__.py | 1 + ..._memory_file_agent_configuration_repository.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 monkey/tests/monkey_island/in_memory_file_agent_configuration_repository.py diff --git a/monkey/tests/monkey_island/__init__.py b/monkey/tests/monkey_island/__init__.py index aed4d07f3..8f8ae7404 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_file_agent_configuration_repository import InMemoryFileAgentConfigurationRepository diff --git a/monkey/tests/monkey_island/in_memory_file_agent_configuration_repository.py b/monkey/tests/monkey_island/in_memory_file_agent_configuration_repository.py new file mode 100644 index 000000000..955e78878 --- /dev/null +++ b/monkey/tests/monkey_island/in_memory_file_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 InMemoryFileAgentConfigurationRepository(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 From d7329ea83942587c2cb49b04b3796c1318c2802f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Jun 2022 14:31:51 +0200 Subject: [PATCH 27/29] UT: Add tests for AgentConfiguration resource --- .../cc/resources/test_agent_configuration.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_configuration.py 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..5cac629e0 --- /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 InMemoryFileAgentConfigurationRepository +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, InMemoryFileAgentConfigurationRepository) + + 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 From 42c4803376e2f6792ae33bbd4fab2fe4b23331b3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Jun 2022 15:56:41 +0200 Subject: [PATCH 28/29] Island: Use schema.loads in AgentConfiguration POST method --- monkey/monkey_island/cc/resources/agent_configuration.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/agent_configuration.py b/monkey/monkey_island/cc/resources/agent_configuration.py index ed0258471..f0ad73cb8 100644 --- a/monkey/monkey_island/cc/resources/agent_configuration.py +++ b/monkey/monkey_island/cc/resources/agent_configuration.py @@ -26,9 +26,7 @@ class AgentConfiguration(AbstractResource): def post(self): try: - request_contents = json.loads(request.data) - - configuration_object = self._schema.load(request_contents) + 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: From 1ae3bd4b4f826909ae2e0883301b0975838d95eb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Jun 2022 17:15:50 +0200 Subject: [PATCH 29/29] UT: Rename InMemoryFileAgentConfigurationRepository to InMemoryAgentConfigurationRepository --- monkey/tests/monkey_island/__init__.py | 2 +- ...ository.py => in_memory_agent_configuration_repository.py} | 2 +- .../monkey_island/cc/resources/test_agent_configuration.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename monkey/tests/monkey_island/{in_memory_file_agent_configuration_repository.py => in_memory_agent_configuration_repository.py} (86%) diff --git a/monkey/tests/monkey_island/__init__.py b/monkey/tests/monkey_island/__init__.py index 8f8ae7404..8f3654a6e 100644 --- a/monkey/tests/monkey_island/__init__.py +++ b/monkey/tests/monkey_island/__init__.py @@ -1,4 +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_file_agent_configuration_repository import InMemoryFileAgentConfigurationRepository +from .in_memory_agent_configuration_repository import InMemoryAgentConfigurationRepository diff --git a/monkey/tests/monkey_island/in_memory_file_agent_configuration_repository.py b/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py similarity index 86% rename from monkey/tests/monkey_island/in_memory_file_agent_configuration_repository.py rename to monkey/tests/monkey_island/in_memory_agent_configuration_repository.py index 955e78878..e737d645c 100644 --- a/monkey/tests/monkey_island/in_memory_file_agent_configuration_repository.py +++ b/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py @@ -4,7 +4,7 @@ from common.configuration.agent_configuration import AgentConfigurationSchema from monkey_island.cc.repository import IAgentConfigurationRepository -class InMemoryFileAgentConfigurationRepository(IAgentConfigurationRepository): +class InMemoryAgentConfigurationRepository(IAgentConfigurationRepository): def __init__(self): self._configuration = AgentConfigurationSchema().load(AGENT_CONFIGURATION) 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 index 5cac629e0..874500b81 100644 --- 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 @@ -3,7 +3,7 @@ import json import pytest from tests.common import StubDIContainer from tests.common.example_agent_configuration import AGENT_CONFIGURATION -from tests.monkey_island import InMemoryFileAgentConfigurationRepository +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 @@ -14,7 +14,7 @@ from monkey_island.cc.resources.agent_configuration import AgentConfiguration def flask_client(build_flask_client): container = StubDIContainer() - container.register(IAgentConfigurationRepository, InMemoryFileAgentConfigurationRepository) + container.register(IAgentConfigurationRepository, InMemoryAgentConfigurationRepository) with build_flask_client(container) as flask_client: yield flask_client