diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index 9c6f01004..c10545c7f 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -2,3 +2,4 @@ Used for a common things between agent and island """ from .di_container import DIContainer, UnregisteredTypeError +from .operating_systems import OperatingSystems diff --git a/monkey/common/configuration/__init__.py b/monkey/common/configuration/__init__.py new file mode 100644 index 000000000..fc1f3c84d --- /dev/null +++ b/monkey/common/configuration/__init__.py @@ -0,0 +1,15 @@ +from .agent_configuration import AgentConfiguration, InvalidConfigurationError +from .agent_sub_configurations import ( + CustomPBAConfiguration, + PluginConfiguration, + ScanTargetConfiguration, + ICMPScanConfiguration, + TCPScanConfiguration, + NetworkScanConfiguration, + ExploitationOptionsConfiguration, + ExploitationConfiguration, + PropagationConfiguration, +) +from .default_agent_configuration import ( + DEFAULT_AGENT_CONFIGURATION, +) diff --git a/monkey/common/configuration/agent_configuration.py b/monkey/common/configuration/agent_configuration.py new file mode 100644 index 000000000..097b86382 --- /dev/null +++ b/monkey/common/configuration/agent_configuration.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, List, Mapping + +from marshmallow import Schema, fields +from marshmallow.exceptions import MarshmallowError + +from .agent_sub_configuration_schemas import ( + CustomPBAConfigurationSchema, + PluginConfigurationSchema, + PropagationConfigurationSchema, +) +from .agent_sub_configurations import ( + CustomPBAConfiguration, + PluginConfiguration, + PropagationConfiguration, +) + + +class InvalidConfigurationError(Exception): + pass + + +INVALID_CONFIGURATION_ERROR_MESSAGE = ( + "Cannot construct an AgentConfiguration object with the supplied, invalid data:" +) + + +@dataclass(frozen=True) +class AgentConfiguration: + keep_tunnel_open_time: float + custom_pbas: CustomPBAConfiguration + post_breach_actions: List[PluginConfiguration] + credential_collectors: List[PluginConfiguration] + payloads: List[PluginConfiguration] + propagation: PropagationConfiguration + + def __post_init__(self): + # This will raise an exception if the object is invalid. Calling this in __post__init() + # makes it impossible to construct an invalid object + try: + AgentConfigurationSchema().dump(self) + except Exception as err: + raise InvalidConfigurationError(f"{INVALID_CONFIGURATION_ERROR_MESSAGE}: {err}") + + @staticmethod + def from_mapping(config_mapping: Mapping[str, Any]) -> AgentConfiguration: + """ + Construct an AgentConfiguration from a Mapping + + :param config_mapping: A Mapping that represents an AgentConfiguration + :return: An AgentConfiguration + :raises: InvalidConfigurationError if the provided Mapping does not represent a valid + AgentConfiguration + """ + + try: + config_dict = AgentConfigurationSchema().load(config_mapping) + return AgentConfiguration(**config_dict) + except MarshmallowError as err: + raise InvalidConfigurationError(f"{INVALID_CONFIGURATION_ERROR_MESSAGE}: {err}") + + @staticmethod + def from_json(config_json: str) -> AgentConfiguration: + """ + Construct an AgentConfiguration from a JSON string + + :param config_json: A JSON string that represents an AgentConfiguration + :return: An AgentConfiguration + :raises: InvalidConfigurationError if the provided JSON does not represent a valid + AgentConfiguration + """ + try: + config_dict = AgentConfigurationSchema().loads(config_json) + return AgentConfiguration(**config_dict) + except MarshmallowError as err: + raise InvalidConfigurationError(f"{INVALID_CONFIGURATION_ERROR_MESSAGE}: {err}") + + @staticmethod + def to_json(config: AgentConfiguration) -> str: + """ + Serialize an AgentConfiguration to JSON + + :param config: An AgentConfiguration + :return: A JSON string representing the AgentConfiguration + """ + return AgentConfigurationSchema().dumps(config) + + +class AgentConfigurationSchema(Schema): + keep_tunnel_open_time = fields.Float() + custom_pbas = fields.Nested(CustomPBAConfigurationSchema) + post_breach_actions = fields.List(fields.Nested(PluginConfigurationSchema)) + credential_collectors = fields.List(fields.Nested(PluginConfigurationSchema)) + payloads = fields.List(fields.Nested(PluginConfigurationSchema)) + propagation = fields.Nested(PropagationConfigurationSchema) diff --git a/monkey/common/configuration/agent_sub_configuration_schemas.py b/monkey/common/configuration/agent_sub_configuration_schemas.py new file mode 100644 index 000000000..bf4d3b8c7 --- /dev/null +++ b/monkey/common/configuration/agent_sub_configuration_schemas.py @@ -0,0 +1,100 @@ +from marshmallow import Schema, fields, post_load + +from .agent_sub_configurations import ( + CustomPBAConfiguration, + ExploitationConfiguration, + ExploitationOptionsConfiguration, + ICMPScanConfiguration, + NetworkScanConfiguration, + PluginConfiguration, + PropagationConfiguration, + ScanTargetConfiguration, + TCPScanConfiguration, +) + + +class CustomPBAConfigurationSchema(Schema): + linux_command = fields.Str() + linux_filename = fields.Str() + windows_command = fields.Str() + windows_filename = fields.Str() + + @post_load + def _make_custom_pba_configuration(self, data, **kwargs): + return CustomPBAConfiguration(**data) + + +class PluginConfigurationSchema(Schema): + name = fields.Str() + options = fields.Mapping() + + @post_load + def _make_plugin_configuration(self, data, **kwargs): + return PluginConfiguration(**data) + + +class ScanTargetConfigurationSchema(Schema): + blocked_ips = fields.List(fields.Str()) + inaccessible_subnets = fields.List(fields.Str()) + local_network_scan = fields.Bool() + subnets = fields.List(fields.Str()) + + @post_load + def _make_scan_target_configuration(self, data, **kwargs): + return ScanTargetConfiguration(**data) + + +class ICMPScanConfigurationSchema(Schema): + timeout = fields.Float() + + @post_load + def _make_icmp_scan_configuration(self, data, **kwargs): + return ICMPScanConfiguration(**data) + + +class TCPScanConfigurationSchema(Schema): + timeout = fields.Float() + ports = fields.List(fields.Int()) + + @post_load + def _make_tcp_scan_configuration(self, data, **kwargs): + return TCPScanConfiguration(**data) + + +class NetworkScanConfigurationSchema(Schema): + tcp = fields.Nested(TCPScanConfigurationSchema) + icmp = fields.Nested(ICMPScanConfigurationSchema) + fingerprinters = fields.List(fields.Nested(PluginConfigurationSchema)) + targets = fields.Nested(ScanTargetConfigurationSchema) + + @post_load + def _make_network_scan_configuration(self, data, **kwargs): + return NetworkScanConfiguration(**data) + + +class ExploitationOptionsConfigurationSchema(Schema): + http_ports = fields.List(fields.Int()) + + @post_load + def _make_exploitation_options_configuration(self, data, **kwargs): + return ExploitationOptionsConfiguration(**data) + + +class ExploitationConfigurationSchema(Schema): + options = fields.Nested(ExploitationOptionsConfigurationSchema) + brute_force = fields.List(fields.Nested(PluginConfigurationSchema)) + vulnerability = fields.List(fields.Nested(PluginConfigurationSchema)) + + @post_load + def _make_exploitation_options_configuration(self, data, **kwargs): + return ExploitationConfiguration(**data) + + +class PropagationConfigurationSchema(Schema): + maximum_depth = fields.Int() + network_scan = fields.Nested(NetworkScanConfigurationSchema) + exploitation = fields.Nested(ExploitationConfigurationSchema) + + @post_load + def _make_propagation_configuration(self, data, **kwargs): + return PropagationConfiguration(**data) diff --git a/monkey/common/configuration/agent_sub_configurations.py b/monkey/common/configuration/agent_sub_configurations.py new file mode 100644 index 000000000..d93b4d774 --- /dev/null +++ b/monkey/common/configuration/agent_sub_configurations.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass +from typing import Dict, List + + +@dataclass(frozen=True) +class CustomPBAConfiguration: + linux_command: str + linux_filename: str + windows_command: str + windows_filename: str + + +@dataclass(frozen=True) +class PluginConfiguration: + name: str + options: Dict + + +@dataclass(frozen=True) +class ScanTargetConfiguration: + blocked_ips: List[str] + inaccessible_subnets: List[str] + local_network_scan: bool + subnets: List[str] + + +@dataclass(frozen=True) +class ICMPScanConfiguration: + timeout: float + + +@dataclass(frozen=True) +class TCPScanConfiguration: + timeout: float + ports: List[int] + + +@dataclass(frozen=True) +class NetworkScanConfiguration: + tcp: TCPScanConfiguration + icmp: ICMPScanConfiguration + fingerprinters: List[PluginConfiguration] + targets: ScanTargetConfiguration + + +@dataclass(frozen=True) +class ExploitationOptionsConfiguration: + http_ports: List[int] + + +@dataclass(frozen=True) +class ExploitationConfiguration: + options: ExploitationOptionsConfiguration + brute_force: List[PluginConfiguration] + vulnerability: List[PluginConfiguration] + + +@dataclass(frozen=True) +class PropagationConfiguration: + maximum_depth: int + network_scan: NetworkScanConfiguration + exploitation: ExploitationConfiguration diff --git a/monkey/common/configuration/default_agent_configuration.py b/monkey/common/configuration/default_agent_configuration.py new file mode 100644 index 000000000..251676017 --- /dev/null +++ b/monkey/common/configuration/default_agent_configuration.py @@ -0,0 +1,114 @@ +from . import AgentConfiguration +from .agent_sub_configurations import ( + CustomPBAConfiguration, + ExploitationConfiguration, + ExploitationOptionsConfiguration, + ICMPScanConfiguration, + NetworkScanConfiguration, + PluginConfiguration, + PropagationConfiguration, + ScanTargetConfiguration, + TCPScanConfiguration, +) + +PBAS = [ + "CommunicateAsBackdoorUser", + "ModifyShellStartupFiles", + "HiddenFiles", + "TrapCommand", + "ChangeSetuidSetgid", + "ScheduleJobs", + "Timestomping", + "AccountDiscovery", + "ProcessListCollection", +] + +CREDENTIAL_COLLECTORS = ["MimikatzCollector", "SSHCollector"] + +PBA_CONFIGURATION = [PluginConfiguration(pba, {}) for pba in PBAS] +CREDENTIAL_COLLECTOR_CONFIGURATION = [ + PluginConfiguration(collector, {}) for collector in CREDENTIAL_COLLECTORS +] + +RANSOMWARE_OPTIONS = { + "encryption": { + "enabled": True, + "directories": {"linux_target_dir": "", "windows_target_dir": ""}, + }, + "other_behaviors": {"readme": True}, +} + +PAYLOAD_CONFIGURATION = [PluginConfiguration("ransomware", RANSOMWARE_OPTIONS)] + +CUSTOM_PBA_CONFIGURATION = CustomPBAConfiguration( + linux_command="", linux_filename="", windows_command="", windows_filename="" +) + +TCP_PORTS = [ + 22, + 80, + 135, + 443, + 445, + 2222, + 3306, + 3389, + 5985, + 5986, + 7001, + 8008, + 8080, + 8088, + 8983, + 9200, + 9600, +] + +TCP_SCAN_CONFIGURATION = TCPScanConfiguration(timeout=3.0, ports=TCP_PORTS) +ICMP_CONFIGURATION = ICMPScanConfiguration(timeout=1.0) +HTTP_PORTS = [80, 443, 7001, 8008, 8080, 8983, 9200, 9600] +FINGERPRINTERS = [ + PluginConfiguration("elastic", {}), + PluginConfiguration("http", {"http_ports": HTTP_PORTS}), + PluginConfiguration("mssql", {}), + PluginConfiguration("smb", {}), + PluginConfiguration("ssh", {}), +] + +SCAN_TARGET_CONFIGURATION = ScanTargetConfiguration([], [], True, []) +NETWORK_SCAN_CONFIGURATION = NetworkScanConfiguration( + TCP_SCAN_CONFIGURATION, ICMP_CONFIGURATION, FINGERPRINTERS, SCAN_TARGET_CONFIGURATION +) + +EXPLOITATION_OPTIONS_CONFIGURATION = ExploitationOptionsConfiguration(HTTP_PORTS) +BRUTE_FORCE_EXPLOITERS = [ + PluginConfiguration("MSSQLExploiter", {}), + PluginConfiguration("PowerShellExploiter", {}), + PluginConfiguration("SSHExploiter", {}), + PluginConfiguration("SmbExploiter", {"smb_download_timeout": 30}), + PluginConfiguration("WmiExploiter", {"smb_download_timeout": 30}), +] + +VULNERABILITY_EXPLOITERS = [ + PluginConfiguration("Log4ShellExploiter", {}), + PluginConfiguration("HadoopExploiter", {}), +] + +EXPLOITATION_CONFIGURATION = ExploitationConfiguration( + EXPLOITATION_OPTIONS_CONFIGURATION, BRUTE_FORCE_EXPLOITERS, VULNERABILITY_EXPLOITERS +) + +PROPAGATION_CONFIGURATION = PropagationConfiguration( + maximum_depth=2, + network_scan=NETWORK_SCAN_CONFIGURATION, + exploitation=EXPLOITATION_CONFIGURATION, +) + +DEFAULT_AGENT_CONFIGURATION = AgentConfiguration( + keep_tunnel_open_time=30, + custom_pbas=CUSTOM_PBA_CONFIGURATION, + post_breach_actions=PBA_CONFIGURATION, + credential_collectors=CREDENTIAL_COLLECTOR_CONFIGURATION, + payloads=PAYLOAD_CONFIGURATION, + propagation=PROPAGATION_CONFIGURATION, +) diff --git a/monkey/common/operating_systems.py b/monkey/common/operating_systems.py new file mode 100644 index 000000000..2ac2f64b3 --- /dev/null +++ b/monkey/common/operating_systems.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class OperatingSystems(Enum): + """ + An Enum representing all supported operating systems + + This Enum represents all operating systems that Infection Monkey supports. The value of each + member is the member's name in all lower-case characters. + """ + + LINUX = "linux" + WINDOWS = "windows" diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 5935145e7..31cebca32 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -38,5 +38,7 @@ class DomainControllerNameFetchError(FailedExploitationError): """Raise on failed attempt to extract domain controller's name""" +# TODO: This has been replaced by common.configuration.InvalidConfigurationError. Use that error +# instead and remove this one. class InvalidConfigurationError(Exception): """Raise when configuration is invalid""" diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index e5e53d39d..7df872e2d 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -24,6 +24,8 @@ pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requ pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not created with sys_platform win32 requirement if not explicitly specified pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows paramiko = {editable = true, ref = "2.10.3.dev1", git = "https://github.com/VakarisZ/paramiko.git"} +marshmallow = "*" +marshmallow-enum = "*" [dev-packages] ldap3 = "*" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 879739998..48f0e9b87 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c1c28510b728242624129b39bd9ace9ba2629061bf64226cb43f1f857fc87212" + "sha256": "202bc4667be3a990bdf39be19ce2ddbacc0b3189810d89f75464ad361c9142b2" }, "pipfile-spec": 6, "requires": { @@ -71,26 +71,28 @@ }, "bcrypt": { "hashes": [ - "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d", - "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29", - "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7", - "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34", - "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7", - "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55", - "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd", - "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6", - "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", - "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" + "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521", + "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb", + "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e", + "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26", + "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a", + "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e", + "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa", + "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129", + "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb", + "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40", + "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa" ], "markers": "python_version >= '3.6'", - "version": "==3.2.0" + "version": "==3.2.2" }, "certifi": { "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], - "version": "==2021.10.8" + "markers": "python_version >= '3.6'", + "version": "==2022.6.15" }, "cffi": { "hashes": [ @@ -160,24 +162,24 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3'", + "markers": "python_full_version >= '3.5.0'", "version": "==2.0.12" }, "click": { "hashes": [ - "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", - "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], "markers": "python_version >= '3.7'", - "version": "==8.1.2" + "version": "==8.1.3" }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", + "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" + "version": "==0.4.5" }, "constantly": { "hashes": [ @@ -188,29 +190,31 @@ }, "cryptography": { "hashes": [ - "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b", - "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51", - "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7", - "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d", - "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6", - "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29", - "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9", - "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf", - "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815", - "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf", - "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85", - "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77", - "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86", - "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb", - "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e", - "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0", - "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3", - "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84", - "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", - "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" + "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804", + "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178", + "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717", + "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982", + "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004", + "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe", + "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452", + "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336", + "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4", + "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15", + "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d", + "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c", + "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0", + "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06", + "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9", + "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1", + "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023", + "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de", + "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f", + "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181", + "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e", + "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a" ], "markers": "python_version >= '3.6'", - "version": "==36.0.2" + "version": "==37.0.2" }, "dnspython": { "hashes": [ @@ -222,11 +226,11 @@ }, "flask": { "hashes": [ - "sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264", - "sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8" + "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477", + "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.1.2" }, "future": { "hashes": [ @@ -247,23 +251,23 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.3" }, "impacket": { "hashes": [ - "sha256:18d557d387f4914fafa739813b9172bc3f8bd9c036e93bf589a8e0ebb7304bba" + "sha256:b8eb020a2cbb47146669cfe31c64bb2e7d6499d049c493d6418b9716f5c74583" ], "index": "pypi", - "version": "==0.9.24" + "version": "==0.10.0" }, "importlib-metadata": { "hashes": [ - "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", - "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" + "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700", + "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec" ], "markers": "python_version < '3.8'", - "version": "==4.11.3" + "version": "==4.11.4" }, "incremental": { "hashes": [ @@ -290,11 +294,11 @@ }, "jinja2": { "hashes": [ - "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119", - "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9" + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" ], "markers": "python_version >= '3.7'", - "version": "==3.1.1" + "version": "==3.1.2" }, "ldap3": { "hashes": [ @@ -368,6 +372,22 @@ "markers": "python_version >= '3.7'", "version": "==2.1.1" }, + "marshmallow": { + "hashes": [ + "sha256:53a1e0ee69f79e1f3e80d17393b25cfc917eda52f859e8183b4af72c3390c1f1", + "sha256:a762c1d8b2bcb0e5c8e964850d03f9f3bffd6a12b626f3c14b9d6b1841999af5" + ], + "index": "pypi", + "version": "==3.16.0" + }, + "marshmallow-enum": { + "hashes": [ + "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58", + "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072" + ], + "index": "pypi", + "version": "==1.5.1" + }, "minidump": { "hashes": [ "sha256:6a9d2152f76ae633c609e09b48b42f55bd5a6b65f920dbbec756e5d9134a6201", @@ -442,6 +462,14 @@ ], "version": "==1.3.0" }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "markers": "python_version >= '3.6'", + "version": "==21.3" + }, "paramiko": { "editable": true, "git": "https://github.com/VakarisZ/paramiko.git", @@ -456,10 +484,11 @@ }, "pefile": { "hashes": [ - "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" + "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b" ], + "index": "pypi", "markers": "sys_platform == 'win32'", - "version": "==2021.9.3" + "version": "==2022.5.30" }, "prompt-toolkit": { "hashes": [ @@ -471,41 +500,41 @@ }, "psutil": { "hashes": [ - "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5", - "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a", - "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4", - "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841", - "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d", - "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d", - "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0", - "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845", - "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf", - "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b", - "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07", - "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618", - "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2", - "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd", - "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666", - "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce", - "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3", - "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d", - "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25", - "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492", - "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b", - "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d", - "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2", - "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203", - "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2", - "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94", - "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9", - "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64", - "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56", - "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3", - "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c", - "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3" + "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685", + "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc", + "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36", + "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1", + "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329", + "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81", + "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de", + "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4", + "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574", + "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237", + "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22", + "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b", + "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0", + "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954", + "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021", + "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537", + "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87", + "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0", + "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc", + "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af", + "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4", + "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453", + "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689", + "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8", + "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680", + "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e", + "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9", + "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b", + "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d", + "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2", + "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5", + "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676" ], "index": "pypi", - "version": "==5.9.0" + "version": "==5.9.1" }, "pyasn1": { "hashes": [ @@ -603,11 +632,11 @@ }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:9765e68552803327d58f6c5eca970bb245b7cdf073e2f912a2a3cb50360bc2d8", - "sha256:9fa4ca03d058cba676c3cc16005076ce6a529f144c08b87c69998625fbd84e0a" + "sha256:5fdb97dcae177955db7ab27840cba97b89dc0c7f4fd9142bba0f9b8d8df85c48", + "sha256:6675634279cfe9e475580fb310c3d557037baefb065e6cb5a69a124361b926fd" ], "markers": "python_version >= '3.7'", - "version": "==2022.3" + "version": "==2022.7" }, "pymssql": { "hashes": [ @@ -670,11 +699,11 @@ }, "pyparsing": { "hashes": [ - "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", - "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" ], "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.8" + "version": "==3.0.9" }, "pypsrp": { "hashes": [ @@ -694,48 +723,66 @@ }, "pysmb": { "hashes": [ - "sha256:298605b8f467ce15b412caaf9af331c135e88fa2172333af14b1b2916361cb6b" + "sha256:3b07db16217465039d0c25694c0705b83663ca82259e209f3566d577536a7395" ], "index": "pypi", - "version": "==1.2.7" + "version": "==1.2.8" }, "pyspnego": { "hashes": [ - "sha256:05438a4e3e1526134bc2d72213417a06a2c3010f5b7271f3122e635e523c3790", - "sha256:12e4da1cbbbd645c0624699a1d99f734161cb9095e9f1fc1c1982ed1b7a44abe", - "sha256:185e0c576cde30d8853d9ea1d69c32cb93e98423934263d6c067bec7adc7dc4f", - "sha256:3361027e7e86de6b784791e09a7b2ba73d06c0be40f027a7be09e45fc92325a5", - "sha256:4971fb166dc9821c98d31d698722d48d0066f1bc63beff8bf3d2a2e60fe507d1", - "sha256:58d352d901baab754f63cb0da790c1f798605eb634f7f922df9bb6822d3de3c5", - "sha256:77b7c75bed737f24989aab453b9b8cd1c1512dfc5bed7a303a1cb1156fd59959", - "sha256:adf2f3e09bc4751c06fab1fedfe734af7f232d79927c753d8981f75a25f791ec", - "sha256:c6993ee6bcfe0036d6246324fcb7975daed858a476bfc7bf1d9334911d3dfca2", - "sha256:e21fc7283caa16761d46bea54e78cbfe3177c21e3b2d17d9ef213edcd86e1250", - "sha256:f05f1a6316a9baeaef243c9420d995c3dc34cfc91841f17db0c793e3fe557728", - "sha256:fe8b2a0d7468d904c61ae63275f8234eb055767aaaba66f6d58d86f47a25aa8e" + "sha256:1fed228edc4b1730844da8237b90489be28c55681cf3934cd04fceb2253e55bf", + "sha256:25fbc90fc6bd16881480316739bab820cc91364765e46340da17f861f89691f1", + "sha256:274b3a2d37e45ad4567bc5754be04b5fefad3f7cdea7d205f739d8a26b5a9189", + "sha256:36db7ec38023a23a545114dfd23825639571f135c72fb4b13a1ed559a0a4d93c", + "sha256:3b1ff3c1d5588b66f8e4ebafa3079a7bf0bdcc6fb144b944c5a101e688a5a280", + "sha256:4b9bda51bd964f40322aa1b33dcfc5d68f23b0680b4b5158175f2e9a04119aa9", + "sha256:5d6d91e35ee63a5de30eb70716bf25274bf16c2c472b046dd21fad60fe63b0b6", + "sha256:7562bc640bf402bb2849f325b0bb41260bd2c0cb06e38b9a8c6f7021b452c873", + "sha256:9c5bdb9f0207e2ce9e5410ee2205bf016755712018534c711ae6c1daff7fa7db", + "sha256:a5c135d20819db3c48f65054d648317f369a61b7b22dc17b9e5ec9c0169541a0", + "sha256:bd95633e7dce69e267579bdbe992fc081a14310236b4e84c3d179b1cf6439ca5", + "sha256:eb41b970dbda0dfe07b1da6fc83fe9f534a66d8dea38c06c0155377697407d9a" ], "markers": "python_version >= '3.6'", - "version": "==0.5.1" + "version": "==0.5.2" }, "pywin32": { - "sys_platform": "== 'win32'", - "version": "==303" + "hashes": [ + "sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc", + "sha256:30c53d6ce44c12a316a06c153ea74152d3b1342610f1b99d40ba2795e5af0269", + "sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3", + "sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48", + "sha256:7ffa0c0fa4ae4077e8b8aa73800540ef8c24530057768c3ac57c609f99a14fd4", + "sha256:94037b5259701988954931333aafd39cf897e990852115656b014ce72e052e96", + "sha256:bb2ea2aa81e96eee6a6b79d87e1d1648d3f8b87f9a64499e0b92b30d141e76df", + "sha256:be253e7b14bc601718f014d2832e4c18a5b023cbe72db826da63df76b77507a1", + "sha256:cbbe34dad39bdbaa2889a424d28752f1b4971939b14b1bb48cbf0182a3bcfc43", + "sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9", + "sha256:d3ee45adff48e0551d1aa60d2ec066fec006083b791f5c3527c40cd8aefac71f", + "sha256:de9827c23321dcf43d2f288f09f3b6d772fee11e809015bdae9e69fe13213988", + "sha256:ead865a2e179b30fb717831f73cf4373401fc62fbc3455a0889a7ddac848f83e", + "sha256:f64c0377cf01b61bd5e76c25e1480ca8ab3b73f0c4add50538d332afdf8f69c5" + ], + "index": "pypi", + "markers": "sys_platform == 'win32'", + "version": "==304" }, "pywin32-ctypes": { "hashes": [ "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==0.2.0" }, "requests": { "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", + "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" ], "index": "pypi", - "version": "==2.27.1" + "version": "==2.28.0" }, "service-identity": { "hashes": [ @@ -746,11 +793,11 @@ }, "setuptools": { "hashes": [ - "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", - "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" + "sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77", + "sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91" ], "markers": "python_version >= '3.7'", - "version": "==62.1.0" + "version": "==62.4.0" }, "six": { "hashes": [ @@ -770,30 +817,48 @@ }, "twisted": { "extras": [ - + "tls" ], "hashes": [ "sha256:a047990f57dfae1e0bd2b7df2526d4f16dcdc843774dc108b78c52f2a5f13680", "sha256:f9f7a91f94932477a9fc3b169d57f54f96c6e74a23d78d9ce54039a7f48928a2" ], - "index": "pypi", + "markers": "python_full_version >= '3.6.7'", "version": "==22.4.0" }, + "twisted-iocpsupport": { + "hashes": [ + "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41", + "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d", + "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9", + "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf", + "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323", + "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32", + "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4", + "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f", + "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546", + "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878", + "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565", + "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415" + ], + "markers": "platform_system == 'Windows'", + "version": "==1.0.2" + }, "typing-extensions": { "hashes": [ - "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", - "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" + "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", + "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], "index": "pypi", - "version": "==4.1.1" + "version": "==4.2.0" }, "unicrypto": { "hashes": [ - "sha256:69240f260493346861e639697e2fda245f14656c7df20ae6d9e6b1bb36acb29e", - "sha256:822bbf18ca6bc17f98c3029470bd8898e51fcb6a7716f6b1c95ed0bf9e0e4da5" + "sha256:0487f9dd9009c326ee9531a79412ae18ad673425a1c800d64947b96fdeb04cdf", + "sha256:fab49ee41926bb31be49552aa135f7aedc04436b49c8fe326d7b6a823810575e" ], "markers": "python_version >= '3.6'", - "version": "==0.0.5" + "version": "==0.0.8" }, "urllib3": { "hashes": [ @@ -812,27 +877,27 @@ }, "werkzeug": { "hashes": [ - "sha256:3c5493ece8268fecdcdc9c0b112211acd006354723b280d643ec732b6d4063d6", - "sha256:f8e89a20aeabbe8a893c24a461d3ee5dad2123b05cc6abd73ceed01d39c3ae74" + "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6", + "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.1.2" }, "winacl": { "hashes": [ - "sha256:187b4394ef247806f50e1d8320bdb9e33ad1f759d9e61e2e391b97b9adf5f58a", - "sha256:949a66b0f46015c8cf8d9c1bfdb3a5174e70c28ae1b096eb778bc2983ea7ce50" + "sha256:1bac567a9d21300082aa2246bb0f94a591fca8e218e163bab18df0e32eefea06", + "sha256:a0b76a327fd337d5ee707ccff95222e6b6ecaa6d887613a1c3d3437ce0be1d4d" ], "markers": "python_version >= '3.6'", - "version": "==0.1.2" + "version": "==0.1.3" }, "winsspi": { "hashes": [ - "sha256:a2ad9c0f6d70f6e0e0d1f54b8582054c62d8a09f346b5ccaf55da68628ca10e1", - "sha256:a64624a25fc2d3663a2c5376c5291f3c7531e9c8051571de9ca9db8bf25746c2" + "sha256:2f5a8d2c4b9f459144426909e26a74e550512e23b6cf9af52c2a00003c7c3fdb", + "sha256:59b7c7595f91414528cfd80c6cfc77ec6f5e4e28185ebd6418f8368ddc7aca82" ], "markers": "python_version >= '3.6'", - "version": "==0.0.9" + "version": "==0.0.10" }, "winsys-3.x": { "hashes": [ @@ -846,6 +911,7 @@ "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==1.5.1" }, diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index fe6106aed..a4c39ee13 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -1,69 +1,4 @@ import uuid -from abc import ABCMeta +# TODO: Find a better place for this GUID = str(uuid.getnode()) - -SENSITIVE_FIELDS = [ - "exploit_password_list", - "exploit_user_list", - "exploit_ssh_keys", -] -LOCAL_CONFIG_VARS = ["name", "id", "max_depth"] -HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden" - - -class Configuration(object): - def from_kv(self, formatted_data): - for key, value in list(formatted_data.items()): - if key.startswith("_"): - continue - if key in LOCAL_CONFIG_VARS: - continue - if hasattr(self, key): - setattr(self, key, value) - if not self.max_depth: - self.max_depth = self.depth - - @staticmethod - def hide_sensitive_info(config_dict): - for field in SENSITIVE_FIELDS: - config_dict[field] = HIDDEN_FIELD_REPLACEMENT_CONTENT - return config_dict - - def as_dict(self): - result = {} - for key in dir(Configuration): - if key.startswith("_"): - continue - try: - value = getattr(self, key) - except AttributeError: - continue - - val_type = type(value) - - if callable(value): - continue - - if val_type in (type, ABCMeta): - value = value.__name__ - elif val_type is tuple or val_type is list: - if len(value) != 0 and type(value[0]) in (type, ABCMeta): - value = val_type([x.__name__ for x in value]) - - result[key] = value - - return result - - ########################### - # monkey config - ########################### - - # depth of propagation - depth = 0 - max_depth = None - - keep_tunnel_open_time = 30 - - -WormConfiguration = Configuration() diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 985778b7c..8d1e48a22 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -1,7 +1,6 @@ import json import logging import platform -from pprint import pformat from socket import gethostname from typing import Mapping, Optional @@ -10,7 +9,7 @@ from requests.exceptions import ConnectionError import infection_monkey.tunnel as tunnel from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT -from infection_monkey.config import GUID, WormConfiguration +from infection_monkey.config import GUID from infection_monkey.network.info import get_host_subnets, local_ips from infection_monkey.transport.http import HTTPConnectProxy from infection_monkey.transport.tcp import TcpProxy @@ -151,38 +150,7 @@ class ControlClient: except Exception as exc: logger.warning(f"Error connecting to control server {self.server_address}: {exc}") - def load_control_config(self): - if not self.server_address: - return - try: - reply = requests.get( # noqa: DUO123 - f"https://{self.server_address}/api/agent/", - verify=False, - proxies=self.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) - - except Exception as exc: - logger.warning(f"Error connecting to control server {self.server_address}: {exc}") - return - - try: - WormConfiguration.from_kv(reply.json().get("config")) - formatted_config = pformat( - WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()) - ) - logger.info(f"New configuration was loaded from server:\n{formatted_config}") - except Exception as exc: - # we don't continue with default conf here because it might be dangerous - logger.error( - "Error parsing JSON reply from control server %s (%s): %s", - self.server_address, - reply._content, - exc, - ) - raise Exception("Couldn't load from from server's configuration, aborting. %s" % exc) - - def create_control_tunnel(self): + def create_control_tunnel(self, keep_tunnel_open_time: int): if not self.server_address: return None @@ -200,7 +168,7 @@ class ControlClient: return tunnel.MonkeyTunnel( proxy_class, - keep_tunnel_open_time=WormConfiguration.keep_tunnel_open_time, + keep_tunnel_open_time=keep_tunnel_open_time, target_addr=target_addr, target_port=target_port, ) diff --git a/monkey/infection_monkey/exploit/caching_agent_repository.py b/monkey/infection_monkey/exploit/caching_agent_repository.py index 0f86bbd9d..7d3580258 100644 --- a/monkey/infection_monkey/exploit/caching_agent_repository.py +++ b/monkey/infection_monkey/exploit/caching_agent_repository.py @@ -5,6 +5,7 @@ from typing import Mapping import requests +from common import OperatingSystems from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from . import IAgentRepository @@ -22,18 +23,22 @@ class CachingAgentRepository(IAgentRepository): self._proxies = proxies self._lock = threading.Lock() - def get_agent_binary(self, os: str, architecture: str = None) -> io.BytesIO: + def get_agent_binary( + self, operating_system: OperatingSystems, architecture: str = None + ) -> io.BytesIO: # If multiple calls to get_agent_binary() are made simultaneously before the result of # _download_binary_from_island() is cached, then multiple requests will be sent to the # island. Add a mutex in front of the call to _download_agent_binary_from_island() so # that only one request per OS will be sent to the island. with self._lock: - return io.BytesIO(self._download_binary_from_island(os)) + return io.BytesIO(self._download_binary_from_island(operating_system)) @lru_cache(maxsize=None) - def _download_binary_from_island(self, os: str) -> bytes: + def _download_binary_from_island(self, operating_system: OperatingSystems) -> bytes: + os_name = operating_system.value + response = requests.get( # noqa: DUO123 - f"{self._island_url}/api/agent-binaries/{os}", + f"{self._island_url}/api/agent-binaries/{os_name}", verify=False, proxies=self._proxies, timeout=MEDIUM_REQUEST_TIMEOUT, diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 8bafa6969..5cf30f23c 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -105,10 +105,10 @@ class HadoopExploiter(WebRCE): def _build_command(self, path, http_path): # Build command to execute monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1) - if "linux" in self.host.os["type"]: - base_command = HADOOP_LINUX_COMMAND - else: + if self.host.is_windows(): base_command = HADOOP_WINDOWS_COMMAND + else: + base_command = HADOOP_LINUX_COMMAND return base_command % { "monkey_path": path, diff --git a/monkey/infection_monkey/exploit/i_agent_repository.py b/monkey/infection_monkey/exploit/i_agent_repository.py index d825772a0..308cf5418 100644 --- a/monkey/infection_monkey/exploit/i_agent_repository.py +++ b/monkey/infection_monkey/exploit/i_agent_repository.py @@ -1,6 +1,8 @@ import abc import io +from common import OperatingSystems + # TODO: The Island also has an IAgentRepository with a totally different interface. At the moment, # the Island and Agent have different needs, but at some point we should unify these. @@ -13,12 +15,13 @@ class IAgentRepository(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def get_agent_binary(self, os: str, architecture: str = None) -> io.BytesIO: + def get_agent_binary( + self, operating_system: OperatingSystems, architecture: str = None + ) -> io.BytesIO: """ Retrieve the appropriate agent binary from the repository. - :param str os: The name of the operating system on which the agent binary will run - :param str architecture: Reserved + :param operating_system: The name of the operating system on which the agent binary will run + :param architecture: Reserved :return: A file-like object for the requested agent binary - :rtype: io.BytesIO """ pass diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 077c7c865..cab4ed548 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -2,6 +2,7 @@ import logging import time from pathlib import PurePath +from common import OperatingSystems from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.utils import Timer from infection_monkey.exploit.log4shell_utils import ( @@ -115,10 +116,10 @@ class Log4ShellExploiter(WebRCE): def _build_command(self, path: PurePath, http_path) -> str: # Build command to execute monkey_cmd = build_monkey_commandline(self.host, self.current_depth + 1, location=path) - if "linux" in self.host.os["type"]: - base_command = LOG4SHELL_LINUX_COMMAND - else: + if self.host.is_windows(): base_command = LOG4SHELL_WINDOWS_COMMAND + else: + base_command = LOG4SHELL_LINUX_COMMAND return base_command % { "monkey_path": path, @@ -128,7 +129,7 @@ class Log4ShellExploiter(WebRCE): } def _build_java_class(self, exploit_command: str) -> bytes: - if "linux" in self.host.os["type"]: + if OperatingSystems.LINUX == self.host.os["type"]: return build_exploit_bytecode(exploit_command, LINUX_EXPLOIT_TEMPLATE_PATH) else: return build_exploit_bytecode(exploit_command, WINDOWS_EXPLOIT_TEMPLATE_PATH) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 6ef72963e..c9991b0b4 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -2,6 +2,7 @@ import logging from pathlib import Path, PurePath from typing import List, Optional +from common import OperatingSystems from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions, get_auth_options from infection_monkey.exploit.powershell_utils.credentials import ( @@ -162,7 +163,7 @@ class PowerShellExploiter(HostExploiter): temp_monkey_binary_filepath.unlink() def _create_local_agent_file(self, binary_path): - agent_binary_bytes = self.agent_repository.get_agent_binary("windows") + agent_binary_bytes = self.agent_repository.get_agent_binary(OperatingSystems.WINDOWS) with open(binary_path, "wb") as f: f.write(agent_binary_bytes.getvalue()) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 0ce1c474e..e268fe4c3 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -15,7 +15,7 @@ AGENT_BINARY_PATH_WIN64 = PureWindowsPath(r"C:\Windows\temp\monkey64.exe") def get_agent_dst_path(host: VictimHost) -> PurePath: - if host.os["type"] == "windows": + if host.is_windows(): path = PureWindowsPath(AGENT_BINARY_PATH_WIN64) else: path = PurePosixPath(AGENT_BINARY_PATH_LINUX) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 92696a5b7..b27f6cf6f 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -57,6 +57,6 @@ class HTTPTools(object): httpd.start() lock.acquire() return ( - "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(host.os["type"])), + f"http://{local_ip}:{local_port}/{urllib.parse.quote(host.os['type'].value)}", httpd, ) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 99438a0a7..45e4b58ee 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -3,6 +3,7 @@ from abc import abstractmethod from posixpath import join from typing import List, Tuple +from common import OperatingSystems from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.http_tools import HTTPTools @@ -162,10 +163,10 @@ class WebRCE(HostExploiter): def get_command(self, path, http_path, commands): try: - if "linux" in self.host.os["type"]: - command = commands["linux"] - else: + if self.host.is_windows(): command = commands["windows"] + else: + command = commands["linux"] # Format command command = command % {"monkey_path": path, "http_path": http_path} except KeyError: @@ -326,7 +327,7 @@ class WebRCE(HostExploiter): :return: response, False if failed and True if permission change is not needed """ logger.info("Changing monkey's permissions") - if "windows" in self.host.os["type"]: + if self.host.is_windows(): logger.info("Permission change not required for windows") return True if not command: @@ -411,13 +412,14 @@ class WebRCE(HostExploiter): :return: Default monkey's destination path for corresponding host or False if failed. """ if not self.host.os.get("type") or ( - self.host.os["type"] != "linux" and self.host.os["type"] != "windows" + self.host.os["type"] != OperatingSystems.LINUX + and self.host.os["type"] != OperatingSystems.WINDOWS ): logger.error("Target's OS was either unidentified or not supported. Aborting") return False - if self.host.os["type"] == "linux": + if self.host.os["type"] == OperatingSystems.LINUX: return DROPPER_TARGET_PATH_LINUX - if self.host.os["type"] == "windows": + if self.host.os["type"] == OperatingSystems.WINDOWS: return DROPPER_TARGET_PATH_WIN64 def get_target_url(self): diff --git a/monkey/infection_monkey/i_control_channel.py b/monkey/infection_monkey/i_control_channel.py index 33539417c..b90308018 100644 --- a/monkey/infection_monkey/i_control_channel.py +++ b/monkey/infection_monkey/i_control_channel.py @@ -1,5 +1,7 @@ import abc +from common.configuration import AgentConfiguration + class IControlChannel(metaclass=abc.ABCMeta): @abc.abstractmethod @@ -11,10 +13,10 @@ class IControlChannel(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def get_config(self) -> dict: + def get_config(self) -> AgentConfiguration: """ - :return: A dictionary containing Agent Configuration - :rtype: dict + :return: An AgentConfiguration object + :rtype: AgentConfiguration """ pass diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 3c71c9721..0102503ca 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -5,13 +5,11 @@ import os import sys import traceback from multiprocessing import freeze_support -from pprint import pformat # dummy import for pyinstaller # noinspection PyUnresolvedReferences import infection_monkey.post_breach # noqa: F401 from common.version import get_version -from infection_monkey.config import WormConfiguration from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import DROPPER_ARG, MONKEY_ARG from infection_monkey.monkey import InfectionMonkey @@ -57,9 +55,6 @@ def main(): mode_args, mode_specific_args = arg_parser.parse_known_args() mode = mode_args.mode - formatted_config = pformat(WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict())) - print(f"Loaded Configuration:\n{formatted_config}") - try: if MONKEY_ARG == mode: log_path = get_agent_log_path() diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index f79fd5f12..214c19004 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,8 +1,9 @@ import logging import threading import time -from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple +from typing import Any, Callable, Iterable, List, Optional +from common.configuration import CustomPBAConfiguration, PluginConfiguration from common.utils import Timer from infection_monkey.credential_store import ICredentialsStore from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError @@ -13,7 +14,7 @@ from infection_monkey.network import NetworkInterface from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -from infection_monkey.utils.propagation import should_propagate +from infection_monkey.utils.propagation import maximum_depth_reached from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter from . import Exploiter, IPScanner, Propagator @@ -111,7 +112,7 @@ class AutomatedMaster(IMaster): time.sleep(CHECK_FOR_TERMINATE_INTERVAL_SEC) @staticmethod - def _try_communicate_with_island(fn: Callable[[], Any], max_tries: int): + def _try_communicate_with_island(fn: Callable[[], Any], max_tries: int) -> Any: tries = 0 while tries < max_tries: try: @@ -141,7 +142,7 @@ class AutomatedMaster(IMaster): try: config = AutomatedMaster._try_communicate_with_island( self._control_channel.get_config, CHECK_FOR_CONFIG_COUNT - )["config"] + ) except IslandCommunicationError as e: logger.error(f"An error occurred while fetching configuration: {e}") return @@ -150,7 +151,7 @@ class AutomatedMaster(IMaster): target=self._run_plugins, name="CredentialCollectorThread", args=( - config["credential_collectors"], + config.credential_collectors, "credential collector", self._collect_credentials, ), @@ -158,7 +159,7 @@ class AutomatedMaster(IMaster): pba_thread = create_daemon_thread( target=self._run_pbas, name="PBAThread", - args=(config["post_breach_actions"].items(), self._run_pba, config["custom_pbas"]), + args=(config.post_breach_actions, self._run_pba, config.custom_pbas), ) credential_collector_thread.start() @@ -173,52 +174,56 @@ class AutomatedMaster(IMaster): current_depth = self._current_depth if self._current_depth is not None else 0 logger.info(f"Current depth is {current_depth}") - if should_propagate(self._control_channel.get_config(), self._current_depth): - self._propagator.propagate(config["propagation"], current_depth, self._stop) + if maximum_depth_reached(config.propagation.maximum_depth, current_depth): + self._propagator.propagate(config.propagation, current_depth, self._stop) else: logger.info("Skipping propagation: maximum depth reached") payload_thread = create_daemon_thread( target=self._run_plugins, name="PayloadThread", - args=(config["payloads"].items(), "payload", self._run_payload), + args=(config.payloads, "payload", self._run_payload), ) payload_thread.start() payload_thread.join() pba_thread.join() - def _collect_credentials(self, collector: str): - credentials = self._puppet.run_credential_collector(collector, {}) + def _collect_credentials(self, collector: PluginConfiguration): + credentials = self._puppet.run_credential_collector(collector.name, collector.options) if credentials: self._telemetry_messenger.send_telemetry(CredentialsTelem(credentials)) else: logger.debug(f"No credentials were collected by {collector}") - def _run_pba(self, pba: Tuple[str, Dict]): - name = pba[0] - options = pba[1] - - for pba_data in self._puppet.run_pba(name, options): + def _run_pba(self, pba: PluginConfiguration): + for pba_data in self._puppet.run_pba(pba.name, pba.options): self._telemetry_messenger.send_telemetry(PostBreachTelem(pba_data)) - def _run_payload(self, payload: Tuple[str, Dict]): - name = payload[0] - options = payload[1] - - self._puppet.run_payload(name, options, self._stop) + def _run_payload(self, payload: PluginConfiguration): + self._puppet.run_payload(payload.name, payload.options, self._stop) def _run_pbas( - self, plugins: Iterable[Any], callback: Callable[[Any], None], custom_pba_options: Mapping + self, + plugins: Iterable[PluginConfiguration], + callback: Callable[[Any], None], + custom_pba_options: CustomPBAConfiguration, ): self._run_plugins(plugins, "post-breach action", callback) if custom_pba_is_enabled(custom_pba_options): - self._run_plugins([("CustomPBA", custom_pba_options)], "post-breach action", callback) + self._run_plugins( + [PluginConfiguration(name="CustomPBA", options=custom_pba_options.__dict__)], + "post-breach action", + callback, + ) def _run_plugins( - self, plugins: Iterable[Any], plugin_type: str, callback: Callable[[Any], None] + self, + plugins: Iterable[PluginConfiguration], + plugin_type: str, + callback: Callable[[Any], None], ): logger.info(f"Running {plugin_type}s") logger.debug(f"Found {len(plugins)} {plugin_type}(s) to run") diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index c93af43e0..d68f42bda 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -1,10 +1,12 @@ import json import logging +from pprint import pformat from typing import Mapping import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT +from common.configuration import AgentConfiguration from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError @@ -47,7 +49,7 @@ class ControlChannel(IControlChannel): ) as e: raise IslandCommunicationError(e) - def get_config(self) -> dict: + def get_config(self) -> AgentConfiguration: try: response = requests.get( # noqa: DUO123 f"https://{self._control_channel_server}/api/agent", @@ -57,7 +59,10 @@ class ControlChannel(IControlChannel): ) response.raise_for_status() - return json.loads(response.content.decode()) + config_dict = json.loads(response.text)["config"] + logger.debug(f"Received configuration:\n{pformat(json.loads(response.text))}") + + return AgentConfiguration.from_mapping(config_dict) except ( json.JSONDecodeError, requests.exceptions.ConnectionError, diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index b9eada5b6..6171576f2 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -5,8 +5,13 @@ from copy import deepcopy from itertools import chain from queue import Queue from threading import Event -from typing import Callable, Dict, List, Mapping +from typing import Callable, Dict, Sequence +from common import OperatingSystems +from common.configuration.agent_sub_configurations import ( + ExploitationConfiguration, + PluginConfiguration, +) from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost @@ -20,6 +25,18 @@ ExploiterName = str Callback = Callable[[ExploiterName, VictimHost, ExploiterResultData], None] +SUPPORTED_OS = { + "HadoopExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], + "Log4ShellExploiter": [OperatingSystems.LINUX, OperatingSystems.WINDOWS], + "MSSQLExploiter": [OperatingSystems.WINDOWS], + "PowerShellExploiter": [OperatingSystems.WINDOWS], + "SSHExploiter": [OperatingSystems.LINUX], + "SmbExploiter": [OperatingSystems.WINDOWS], + "WmiExploiter": [OperatingSystems.WINDOWS], + "ZerologonExploiter": [OperatingSystems.WINDOWS], +} + + class Exploiter: def __init__( self, @@ -33,7 +50,7 @@ class Exploiter: def exploit_hosts( self, - exploiter_config: Dict, + exploiter_config: ExploitationConfiguration, hosts_to_exploit: Queue, current_depth: int, results_callback: Callback, @@ -43,7 +60,7 @@ class Exploiter: exploiters_to_run = self._process_exploiter_config(exploiter_config) logger.debug( "Agent is configured to run the following exploiters in order: " - f"{', '.join([e['name'] for e in exploiters_to_run])}" + f"{', '.join([e.name for e in exploiters_to_run])}" ) exploit_args = ( @@ -62,24 +79,26 @@ class Exploiter: ) @staticmethod - def _process_exploiter_config(exploiter_config: Mapping) -> List[Mapping]: + def _process_exploiter_config( + exploiter_config: ExploitationConfiguration, + ) -> Sequence[PluginConfiguration]: # Run vulnerability exploiters before brute force exploiters to minimize the effect of # account lockout due to invalid credentials - ordered_exploiters = chain( - exploiter_config["vulnerability"], exploiter_config["brute_force"] - ) + ordered_exploiters = chain(exploiter_config.vulnerability, exploiter_config.brute_force) exploiters_to_run = list(deepcopy(ordered_exploiters)) + extended_exploiters = [] for exploiter in exploiters_to_run: # This order allows exploiter-specific options to # override general options for all exploiters. - exploiter["options"] = {**exploiter_config["options"], **exploiter["options"]} + options = {**exploiter_config.options.__dict__, **exploiter.options} + extended_exploiters.append(PluginConfiguration(exploiter.name, options)) - return exploiters_to_run + return extended_exploiters def _exploit_hosts_on_queue( self, - exploiters_to_run: List[Dict], + exploiters_to_run: Sequence[PluginConfiguration], hosts_to_exploit: Queue, current_depth: int, results_callback: Callback, @@ -106,7 +125,7 @@ class Exploiter: def _run_all_exploiters( self, - exploiters_to_run: List[Dict], + exploiters_to_run: Sequence[PluginConfiguration], victim_host: VictimHost, current_depth: int, results_callback: Callback, @@ -114,11 +133,11 @@ class Exploiter: ): for exploiter in interruptible_iter(exploiters_to_run, stop): - exploiter_name = exploiter["name"] + exploiter_name = exploiter.name 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})" @@ -126,7 +145,7 @@ class Exploiter: continue exploiter_results = self._run_exploiter( - exploiter_name, exploiter["options"], victim_host, current_depth, stop + exploiter_name, exploiter.options, victim_host, current_depth, stop ) results_callback(exploiter_name, victim_host, exploiter_results) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 8c0ea5caa..071abe95a 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -3,8 +3,13 @@ import queue import threading from queue import Queue from threading import Event -from typing import Any, Callable, Dict, List +from typing import Callable, Dict, Sequence +from common.configuration.agent_sub_configurations import ( + NetworkScanConfiguration, + PluginConfiguration, + ScanTargetConfiguration, +) from infection_monkey.i_puppet import ( FingerprintData, IPuppet, @@ -29,8 +34,8 @@ class IPScanner: def scan( self, - addresses_to_scan: List[NetworkAddress], - options: Dict, + addresses_to_scan: Sequence[NetworkAddress], + options: ScanTargetConfiguration, results_callback: Callback, stop: Event, ): @@ -49,12 +54,16 @@ class IPScanner: ) def _scan_addresses( - self, addresses: Queue, options: Dict, results_callback: Callback, stop: Event + self, + addresses: Queue, + options: NetworkScanConfiguration, + results_callback: Callback, + stop: Event, ): - logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") - icmp_timeout = options["icmp"]["timeout_ms"] / 1000 - tcp_timeout = options["tcp"]["timeout_ms"] / 1000 - tcp_ports = options["tcp"]["ports"] + logger.debug(f"Starting scan .read -- Thread ID: {threading.get_ident()}") + icmp_timeout = options.icmp.timeout + tcp_timeout = options.tcp.timeout + tcp_ports = options.tcp.ports try: while not stop.is_set(): @@ -66,7 +75,7 @@ class IPScanner: fingerprint_data = {} if IPScanner.port_scan_found_open_port(port_scan_data): - fingerprinters = options["fingerprinters"] + fingerprinters = options.fingerprinters fingerprint_data = self._run_fingerprinters( address.ip, fingerprinters, ping_scan_data, port_scan_data, stop ) @@ -90,7 +99,7 @@ class IPScanner: def _run_fingerprinters( self, ip: str, - fingerprinters: List[Dict[str, Any]], + fingerprinters: Sequence[PluginConfiguration], ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], stop: Event, @@ -98,8 +107,8 @@ class IPScanner: fingerprint_data = {} for f in interruptible_iter(fingerprinters, stop): - fingerprint_data[f["name"]] = self._puppet.fingerprint( - f["name"], ip, ping_scan_data, port_scan_data, f["options"] + fingerprint_data[f.name] = self._puppet.fingerprint( + f.name, ip, ping_scan_data, port_scan_data, f.options ) return fingerprint_data diff --git a/monkey/infection_monkey/master/option_parsing.py b/monkey/infection_monkey/master/option_parsing.py index c35bf6303..c9262c5c9 100644 --- a/monkey/infection_monkey/master/option_parsing.py +++ b/monkey/infection_monkey/master/option_parsing.py @@ -1,13 +1,12 @@ -from typing import Dict - +from common.configuration import CustomPBAConfiguration from infection_monkey.utils.environment import is_windows_os -def custom_pba_is_enabled(pba_options: Dict) -> bool: +def custom_pba_is_enabled(pba_options: CustomPBAConfiguration) -> bool: if not is_windows_os(): - if pba_options["linux_command"]: + if pba_options.linux_command: return True else: - if pba_options["windows_command"]: + if pba_options.windows_command: return True return False diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index be4d6caf2..64edae2ec 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -1,8 +1,13 @@ import logging from queue import Queue from threading import Event -from typing import Dict, List +from typing import List +from common.configuration import ( + NetworkScanConfiguration, + PropagationConfiguration, + ScanTargetConfiguration, +) from infection_monkey.i_puppet import ( ExploiterResultData, FingerprintData, @@ -39,14 +44,18 @@ class Propagator: self._local_network_interfaces = local_network_interfaces self._hosts_to_exploit = None - def propagate(self, propagation_config: Dict, current_depth: int, stop: Event): + def propagate( + self, propagation_config: PropagationConfiguration, current_depth: int, stop: Event + ): logger.info("Attempting to propagate") network_scan_completed = Event() self._hosts_to_exploit = Queue() scan_thread = create_daemon_thread( - target=self._scan_network, name="PropagatorScanThread", args=(propagation_config, stop) + target=self._scan_network, + name="PropagatorScanThread", + args=(propagation_config.network_scan, stop), ) exploit_thread = create_daemon_thread( target=self._exploit_hosts, @@ -64,22 +73,21 @@ class Propagator: logger.info("Finished attempting to propagate") - def _scan_network(self, propagation_config: Dict, stop: Event): + def _scan_network(self, scan_config: NetworkScanConfiguration, stop: Event): logger.info("Starting network scan") - target_config = propagation_config["targets"] - scan_config = propagation_config["network_scan"] - - addresses_to_scan = self._compile_scan_target_list(target_config) + addresses_to_scan = self._compile_scan_target_list(scan_config.targets) self._ip_scanner.scan(addresses_to_scan, scan_config, self._process_scan_results, stop) logger.info("Finished network scan") - def _compile_scan_target_list(self, target_config: Dict) -> List[NetworkAddress]: - ranges_to_scan = target_config["subnet_scan_list"] - inaccessible_subnets = target_config["inaccessible_subnets"] - blocklisted_ips = target_config["blocked_ips"] - enable_local_network_scan = target_config["local_network_scan"] + def _compile_scan_target_list( + self, target_config: ScanTargetConfiguration + ) -> List[NetworkAddress]: + ranges_to_scan = target_config.subnets + inaccessible_subnets = target_config.inaccessible_subnets + blocklisted_ips = target_config.blocked_ips + enable_local_network_scan = target_config.local_network_scan return compile_scan_target_list( self._local_network_interfaces, @@ -134,14 +142,14 @@ class Propagator: def _exploit_hosts( self, - propagation_config: Dict, + propagation_config: PropagationConfiguration, current_depth: int, network_scan_completed: Event, stop: Event, ): logger.info("Exploiting victims") - exploiter_config = propagation_config["exploiters"] + exploiter_config = propagation_config.exploitation self._exploiter.exploit_hosts( exploiter_config, self._hosts_to_exploit, diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index 95cc85810..167bef246 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -1,5 +1,7 @@ from typing import Optional +from common import OperatingSystems + class VictimHost(object): def __init__(self, ip_addr: str, domain_name: str = ""): @@ -14,6 +16,9 @@ class VictimHost(object): def as_dict(self): return self.__dict__ + def is_windows(self) -> bool: + return OperatingSystems.WINDOWS == self.os["type"] + def __hash__(self): return hash(self.ip_addr) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 53d8f8662..749803c7b 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -78,7 +78,7 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_agent_log_path -from infection_monkey.utils.propagation import should_propagate +from infection_monkey.utils.propagation import maximum_depth_reached from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers logger = logging.getLogger(__name__) @@ -152,7 +152,6 @@ class InfectionMonkey: raise Exception(f"Monkey couldn't find server with {self._opts.tunnel} default tunnel.") self._control_client.wakeup(parent=self._opts.parent) - self._control_client.load_control_config() def _current_server_is_set(self) -> bool: if self._control_client.find_server(default_tunnel=self._opts.tunnel): @@ -168,11 +167,17 @@ class InfectionMonkey: if firewall.is_enabled(): firewall.add_firewall_rule() - self._monkey_inbound_tunnel = self._control_client.create_control_tunnel() - config = ControlChannel( + control_channel = ControlChannel( self._control_client.server_address, GUID, self._control_client.proxies - ).get_config() - if self._monkey_inbound_tunnel and should_propagate(config, self._current_depth): + ) + + config = control_channel.get_config() + self._monkey_inbound_tunnel = self._control_client.create_control_tunnel( + config.keep_tunnel_open_time + ) + if self._monkey_inbound_tunnel and maximum_depth_reached( + config.propagation.maximum_depth, self._current_depth + ): self._inbound_tunnel_opened = True self._monkey_inbound_tunnel.start() diff --git a/monkey/infection_monkey/network_scanning/ping_scanner.py b/monkey/infection_monkey/network_scanning/ping_scanner.py index 16fb2df96..cddf4bdd4 100644 --- a/monkey/infection_monkey/network_scanning/ping_scanner.py +++ b/monkey/infection_monkey/network_scanning/ping_scanner.py @@ -5,6 +5,7 @@ import re import subprocess import sys +from common import OperatingSystems from infection_monkey.i_puppet import PingScanData from infection_monkey.utils.environment import is_windows_os @@ -79,9 +80,9 @@ def _process_ping_command_output(ping_command_output: str) -> PingScanData: operating_system = None if ttl <= LINUX_TTL: - operating_system = "linux" + operating_system = OperatingSystems.LINUX else: # as far we we know, could also be OSX/BSD, but lets handle that when it comes up. - operating_system = "windows" + operating_system = OperatingSystems.WINDOWS return PingScanData(True, operating_system) diff --git a/monkey/infection_monkey/network_scanning/smb_fingerprinter.py b/monkey/infection_monkey/network_scanning/smb_fingerprinter.py index d47ce224e..438e13db0 100644 --- a/monkey/infection_monkey/network_scanning/smb_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/smb_fingerprinter.py @@ -5,6 +5,7 @@ from typing import Dict from odict import odict +from common import OperatingSystems from infection_monkey.i_puppet import ( FingerprintData, IFingerprinter, @@ -193,9 +194,9 @@ class SMBFingerprinter(IFingerprinter): logger.debug(f'os_version: "{os_version}", service_client: "{service_client}"') if os_version.lower() != "unix": - os_type = "windows" + os_type = OperatingSystems.WINDOWS else: - os_type = "linux" + os_type = OperatingSystems.LINUX smb_service["name"] = service_client diff --git a/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py b/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py index 32aa20ad9..86eb8f420 100644 --- a/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py @@ -1,6 +1,7 @@ import re from typing import Dict, Optional, Tuple +from common import OperatingSystems from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData SSH_REGEX = r"SSH-\d\.\d-OpenSSH" @@ -40,6 +41,6 @@ class SSHFingerprinter(IFingerprinter): for dist in LINUX_DIST_SSH: if banner.lower().find(dist) != -1: os_version = banner.split(" ").pop().strip() - os = "linux" + os = OperatingSystems.LINUX return os, os_version diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index 2641051cc..7a5a350f5 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -16,7 +16,7 @@ class ClearCommandHistory(PBA): super().__init__(telemetry_messenger, name=POST_BREACH_CLEAR_CMD_HISTORY) def run(self, options: Dict) -> Iterable[PostBreachData]: - results = [pba.run() for pba in self.clear_command_history_pba_list()] + results = [pba.run(options) for pba in self.clear_command_history_pba_list()] if results: # `self.command` is empty here self.pba_data.append(PostBreachData(self.name, self.command, results)) @@ -53,7 +53,7 @@ class ClearCommandHistory(PBA): linux_cmd=linux_cmds, ) - def run(self) -> Tuple[str, bool]: + def run(self, options: Dict) -> Tuple[str, bool]: if self.command: try: output = subprocess.check_output( # noqa: DUO116 diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index 18e8d2158..32b5110f2 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -4,6 +4,7 @@ import logging from infection_monkey.control import ControlClient from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.telem_encoder import TelemetryJSONEncoder logger = logging.getLogger(__name__) LOGGED_DATA_LENGTH = 300 # How many characters of telemetry data will be logged @@ -39,7 +40,7 @@ class BaseTelem(ITelem, metaclass=abc.ABCMeta): @property def json_encoder(self): - return json.JSONEncoder + return TelemetryJSONEncoder def _log_telem_sending(self, serialized_data: str, log_data=True): logger.debug(f"Sending {self.telem_category} telemetry.") diff --git a/monkey/infection_monkey/telemetry/telem_encoder.py b/monkey/infection_monkey/telemetry/telem_encoder.py new file mode 100644 index 000000000..019569107 --- /dev/null +++ b/monkey/infection_monkey/telemetry/telem_encoder.py @@ -0,0 +1,10 @@ +import json + +from common import OperatingSystems + + +class TelemetryJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, OperatingSystems): + return obj.name + return json.JSONEncoder.default(self, obj) diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 63aaa0b36..7bcbcd87d 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -62,7 +62,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): f.close() def send_head(self): - if self.path != "/" + urllib.parse.quote(self.victim_os): + if self.path != "/" + urllib.parse.quote(self.victim_os.value): self.send_error(500, "") return None, 0, 0 try: diff --git a/monkey/infection_monkey/utils/propagation.py b/monkey/infection_monkey/utils/propagation.py index 004bafdd2..2da2e7bee 100644 --- a/monkey/infection_monkey/utils/propagation.py +++ b/monkey/infection_monkey/utils/propagation.py @@ -1,2 +1,2 @@ -def should_propagate(config: dict, current_depth: int) -> bool: - return config["config"]["depth"] > current_depth +def maximum_depth_reached(maximum_depth: int, current_depth: int) -> bool: + return maximum_depth > current_depth diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 8ed291b72..fbc8b16ae 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -28,6 +28,8 @@ cffi = "*" # Without explicit install: ModuleNotFoundError: No module named '_c pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not created with sys_platform win32 requirement if not explicitly specified pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows +marshmallow = "*" +marshmallow-enum = "*" [dev-packages] virtualenv = ">=20.0.26" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 6b3af1043..c3e59c1d5 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "945e5bfe369347efb543a6c8f0ec2bb90df87284d0a65455944479ec2f78a5fa" + "sha256": "04efa1f593acdfcdc48b7089108921a46421acbacec80d8a664ec674b221dd4b" }, "pipfile-spec": 6, "requires": { @@ -72,11 +72,11 @@ }, "certifi": { "hashes": [ - "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", - "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], "markers": "python_version >= '3.6'", - "version": "==2022.5.18.1" + "version": "==2022.6.15" }, "cffi": { "hashes": [ @@ -139,7 +139,7 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3'", + "markers": "python_full_version >= '3.5.0'", "version": "==2.0.12" }, "click": { @@ -152,11 +152,11 @@ }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", + "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" ], "markers": "platform_system == 'Windows'", - "version": "==0.4.4" + "version": "==0.4.5" }, "cryptography": { "hashes": [ @@ -338,7 +338,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.3" }, "importlib-metadata": { @@ -435,6 +435,22 @@ "markers": "python_version >= '3.7'", "version": "==2.1.1" }, + "marshmallow": { + "hashes": [ + "sha256:53a1e0ee69f79e1f3e80d17393b25cfc917eda52f859e8183b4af72c3390c1f1", + "sha256:a762c1d8b2bcb0e5c8e964850d03f9f3bffd6a12b626f3c14b9d6b1841999af5" + ], + "index": "pypi", + "version": "==3.16.0" + }, + "marshmallow-enum": { + "hashes": [ + "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58", + "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072" + ], + "index": "pypi", + "version": "==1.5.1" + }, "mongoengine": { "hashes": [ "sha256:6e127f45f71c2bc5e72461ec297a0c20f04c3ee0bf6dd869e336226e325db6ef", @@ -479,6 +495,14 @@ "index": "pypi", "version": "==0.11.0" }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "markers": "python_version >= '3.6'", + "version": "==21.3" + }, "pefile": { "hashes": [ "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b" @@ -688,6 +712,14 @@ ], "version": "==3.12.3" }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" + }, "pyrsistent": { "hashes": [ "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c", @@ -762,11 +794,11 @@ }, "requests": { "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", + "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" ], "index": "pypi", - "version": "==2.27.1" + "version": "==2.28.0" }, "ring": { "hashes": [ @@ -785,11 +817,11 @@ }, "setuptools": { "hashes": [ - "sha256:68e45d17c9281ba25dc0104eadd2647172b3472d9e01f911efa57965e8d51a36", - "sha256:a43bdedf853c670e5fed28e5623403bad2f73cf02f9a2774e91def6bda8265a7" + "sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77", + "sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91" ], "markers": "python_version >= '3.7'", - "version": "==62.3.2" + "version": "==62.4.0" }, "six": { "hashes": [ @@ -928,11 +960,11 @@ }, "babel": { "hashes": [ - "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2", - "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13" + "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51", + "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb" ], "markers": "python_version >= '3.6'", - "version": "==2.10.1" + "version": "==2.10.3" }, "black": { "hashes": [ @@ -965,18 +997,18 @@ }, "certifi": { "hashes": [ - "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", - "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], "markers": "python_version >= '3.6'", - "version": "==2022.5.18.1" + "version": "==2022.6.15" }, "charset-normalizer": { "hashes": [ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3'", + "markers": "python_full_version >= '3.5.0'", "version": "==2.0.12" }, "click": { @@ -989,11 +1021,11 @@ }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", + "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" ], "markers": "platform_system == 'Windows'", - "version": "==0.4.4" + "version": "==0.4.5" }, "coverage": { "hashes": [ @@ -1085,7 +1117,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.3" }, "imagesize": { @@ -1291,11 +1323,11 @@ }, "requests": { "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", + "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" ], "index": "pypi", - "version": "==2.27.1" + "version": "==2.28.0" }, "requests-mock": { "hashes": [ @@ -1313,11 +1345,11 @@ }, "setuptools": { "hashes": [ - "sha256:68e45d17c9281ba25dc0104eadd2647172b3472d9e01f911efa57965e8d51a36", - "sha256:a43bdedf853c670e5fed28e5623403bad2f73cf02f9a2774e91def6bda8265a7" + "sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77", + "sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91" ], "markers": "python_version >= '3.7'", - "version": "==62.3.2" + "version": "==62.4.0" }, "six": { "hashes": [ diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 9db06d486..e2253b36c 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -12,6 +12,7 @@ from common import DIContainer from monkey_island.cc.database import database, mongo from monkey_island.cc.resources import AgentBinaries, RemoteRun from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.resources.agent_configuration import AgentConfiguration from monkey_island.cc.resources.agent_controls import StopAgentCheck, StopAllAgents from monkey_island.cc.resources.attack.attack_report import AttackReport from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt @@ -155,6 +156,7 @@ def init_api_resources(api: FlaskDIWrapper): api.add_resource(IslandConfiguration) api.add_resource(ConfigurationExport) api.add_resource(ConfigurationImport) + api.add_resource(AgentConfiguration) api.add_resource(AgentBinaries) api.add_resource(NetMap) api.add_resource(Edge) diff --git a/monkey/monkey_island/cc/repository/__init__.py b/monkey/monkey_island/cc/repository/__init__.py index 885d467be..52e6f6873 100644 --- a/monkey/monkey_island/cc/repository/__init__.py +++ b/monkey/monkey_island/cc/repository/__init__.py @@ -1,3 +1,6 @@ -from .file_storage import FileRetrievalError, IFileRepository, LocalStorageFileRepository -from .i_agent_binary_repository import IAgentBinaryRepository, AgentRetrievalError +from .errors import RemovalError, RetrievalError, StorageError +from .file_storage import FileNotFoundError, IFileRepository, LocalStorageFileRepository +from .i_agent_binary_repository import IAgentBinaryRepository from .agent_binary_repository import AgentBinaryRepository +from .i_agent_configuration_repository import IAgentConfigurationRepository +from .file_agent_configuration_repository import FileAgentConfigurationRepository diff --git a/monkey/monkey_island/cc/repository/agent_binary_repository.py b/monkey/monkey_island/cc/repository/agent_binary_repository.py index 4c1334f71..9b753ce00 100644 --- a/monkey/monkey_island/cc/repository/agent_binary_repository.py +++ b/monkey/monkey_island/cc/repository/agent_binary_repository.py @@ -1,6 +1,6 @@ from typing import BinaryIO -from . import AgentRetrievalError, FileRetrievalError, IAgentBinaryRepository, IFileRepository +from . import IAgentBinaryRepository, IFileRepository, RetrievalError LINUX_AGENT_FILE_NAME = "monkey-linux-64" WINDOWS_AGENT_FILE_NAME = "monkey-windows-64.exe" @@ -20,8 +20,8 @@ class AgentBinaryRepository(IAgentBinaryRepository): try: agent_binary = self._file_repository.open_file(filename) return agent_binary - except FileRetrievalError as err: - raise AgentRetrievalError( + except Exception as err: + raise RetrievalError( f"An error occurred while retrieving the {filename}" f" agent binary from {self._file_repository}: {err}" ) diff --git a/monkey/monkey_island/cc/repository/errors.py b/monkey/monkey_island/cc/repository/errors.py new file mode 100644 index 000000000..aeb9fa23c --- /dev/null +++ b/monkey/monkey_island/cc/repository/errors.py @@ -0,0 +1,22 @@ +class RemovalError(RuntimeError): + """ + Raised when a repository encounters an error while attempting to remove data. + """ + + pass + + +class RetrievalError(RuntimeError): + """ + Raised when a repository encounters an error while attempting to retrieve data. + """ + + pass + + +class StorageError(RuntimeError): + """ + Raised when a repository encounters an error while attempting to store data. + """ + + pass diff --git a/monkey/monkey_island/cc/repository/file_agent_configuration_repository.py b/monkey/monkey_island/cc/repository/file_agent_configuration_repository.py new file mode 100644 index 000000000..312e3921e --- /dev/null +++ b/monkey/monkey_island/cc/repository/file_agent_configuration_repository.py @@ -0,0 +1,37 @@ +import io + +from common.configuration import AgentConfiguration +from monkey_island.cc import repository +from monkey_island.cc.repository import ( + IAgentConfigurationRepository, + IFileRepository, + RetrievalError, +) + +AGENT_CONFIGURATION_FILE_NAME = "agent_configuration.json" + + +class FileAgentConfigurationRepository(IAgentConfigurationRepository): + def __init__( + self, default_agent_configuration: AgentConfiguration, file_repository: IFileRepository + ): + self._default_agent_configuration = default_agent_configuration + self._file_repository = file_repository + + def get_configuration(self) -> AgentConfiguration: + try: + with self._file_repository.open_file(AGENT_CONFIGURATION_FILE_NAME) as f: + configuration_json = f.read().decode() + + return AgentConfiguration.from_json(configuration_json) + except repository.FileNotFoundError: + return self._default_agent_configuration + except Exception as err: + raise RetrievalError(f"Error retrieving the agent configuration: {err}") + + def store_configuration(self, agent_configuration: AgentConfiguration): + configuration_json = AgentConfiguration.to_json(agent_configuration) + + self._file_repository.save_file( + AGENT_CONFIGURATION_FILE_NAME, io.BytesIO(configuration_json.encode()) + ) diff --git a/monkey/monkey_island/cc/repository/file_storage/__init__.py b/monkey/monkey_island/cc/repository/file_storage/__init__.py index 30b59b5d9..14227bc66 100644 --- a/monkey/monkey_island/cc/repository/file_storage/__init__.py +++ b/monkey/monkey_island/cc/repository/file_storage/__init__.py @@ -1,2 +1,2 @@ -from .i_file_repository import IFileRepository, FileRetrievalError +from .i_file_repository import IFileRepository, FileNotFoundError from .local_storage_file_repository import LocalStorageFileRepository diff --git a/monkey/monkey_island/cc/repository/file_storage/i_file_repository.py b/monkey/monkey_island/cc/repository/file_storage/i_file_repository.py index a0865b797..baa52fdda 100644 --- a/monkey/monkey_island/cc/repository/file_storage/i_file_repository.py +++ b/monkey/monkey_island/cc/repository/file_storage/i_file_repository.py @@ -1,8 +1,10 @@ import abc from typing import BinaryIO +from monkey_island.cc.repository import RetrievalError -class FileRetrievalError(RuntimeError): + +class FileNotFoundError(RetrievalError): pass @@ -18,6 +20,7 @@ class IFileRepository(metaclass=abc.ABCMeta): :param unsafe_file_name: An unsanitized file name that will identify the file :param file_contents: The data to be stored in the file + :raises StorageError: If an error was encountered while attempting to store the file """ pass @@ -28,7 +31,8 @@ class IFileRepository(metaclass=abc.ABCMeta): :param unsafe_file_name: An unsanitized file name that identifies the file to be opened :return: A file-like object providing access to the file's contents - :raises FileRetrievalError: if the file cannot be opened + :raises FileNotFoundError: if the file does not exist + :raises RetrievalError: if the file exists but cannot be retrieved """ pass @@ -41,6 +45,7 @@ class IFileRepository(metaclass=abc.ABCMeta): idempotent and will succeed if the file to be deleted does not exist. :param unsafe_file_name: An unsanitized file name that identifies the file to be deleted + :raises RemovalError: If an error was encountered while attempting to remove a file """ pass @@ -48,5 +53,7 @@ class IFileRepository(metaclass=abc.ABCMeta): def delete_all_files(self): """ Delete all files that have been stored using this service. + + :raises RemovalError: If an error was encountered while attempting to remove a file """ pass diff --git a/monkey/monkey_island/cc/repository/file_storage/local_storage_file_repository.py b/monkey/monkey_island/cc/repository/file_storage/local_storage_file_repository.py index 4202496ad..4c7662b0c 100644 --- a/monkey/monkey_island/cc/repository/file_storage/local_storage_file_repository.py +++ b/monkey/monkey_island/cc/repository/file_storage/local_storage_file_repository.py @@ -4,9 +4,10 @@ from pathlib import Path from typing import BinaryIO from common.utils.file_utils import get_all_regular_files_in_directory +from monkey_island.cc.repository import RemovalError, RetrievalError, StorageError from monkey_island.cc.server_utils.file_utils import create_secure_directory -from . import FileRetrievalError, IFileRepository +from . import IFileRepository, i_file_repository logger = logging.getLogger(__name__) @@ -31,34 +32,42 @@ class LocalStorageFileRepository(IFileRepository): self._storage_directory = storage_directory def save_file(self, unsafe_file_name: str, file_contents: BinaryIO): - safe_file_path = self._get_safe_file_path(unsafe_file_name) + try: + safe_file_path = self._get_safe_file_path(unsafe_file_name) - logger.debug(f"Saving file contents to {safe_file_path}") - with open(safe_file_path, "wb") as dest: - shutil.copyfileobj(file_contents, dest) + logger.debug(f"Saving file contents to {safe_file_path}") + with open(safe_file_path, "wb") as dest: + shutil.copyfileobj(file_contents, dest) + except Exception as err: + raise StorageError(f"Error while attempting to store {unsafe_file_name}: {err}") def open_file(self, unsafe_file_name: str) -> BinaryIO: - safe_file_path = self._get_safe_file_path(unsafe_file_name) - try: + safe_file_path = self._get_safe_file_path(unsafe_file_name) + logger.debug(f"Opening {safe_file_path}") return open(safe_file_path, "rb") - except OSError as err: - # TODO: The interface should make a destinction between file not found and an error when - # retrieving a file that should exist. The built-in `FileNotFoundError` is not - # sufficient because it inherits from `OSError` and the interface does not - # guarantee that the file is stored on the local file system. - raise FileRetrievalError(f"Failed to retrieve file {safe_file_path}: {err}") from err + except FileNotFoundError as err: + # Wrap Python's FileNotFound error, which is-an OSError, in repository.FileNotFoundError + raise i_file_repository.FileNotFoundError( + f'The requested file "{unsafe_file_name}" does not exist: {err}' + ) + except Exception as err: + raise RetrievalError( + f'Error retrieving file "{unsafe_file_name}" from the repository: {err}' + ) def delete_file(self, unsafe_file_name: str): - safe_file_path = self._get_safe_file_path(unsafe_file_name) - try: + safe_file_path = self._get_safe_file_path(unsafe_file_name) + logger.debug(f"Deleting {safe_file_path}") safe_file_path.unlink() except FileNotFoundError: # This method is idempotent. pass + except Exception as err: + raise RemovalError(f"Error while attempting to remove {unsafe_file_name}: {err}") def _get_safe_file_path(self, unsafe_file_name: str): # Remove any path information from the file name. @@ -67,12 +76,17 @@ class LocalStorageFileRepository(IFileRepository): # This is a paranoid check to avoid directory traversal attacks. if self._storage_directory.resolve() not in safe_file_path.parents: - raise ValueError(f"The file named {unsafe_file_name} can not be safely retrieved") + raise ValueError( + f'The file named "{unsafe_file_name}" cannot be safely retrieved or written' + ) logger.debug(f"Untrusted file name {unsafe_file_name} sanitized: {safe_file_path}") return safe_file_path def delete_all_files(self): - for file in get_all_regular_files_in_directory(self._storage_directory): - logger.debug(f"Deleting {file}") - file.unlink() + try: + for file in get_all_regular_files_in_directory(self._storage_directory): + logger.debug(f"Deleting {file}") + file.unlink() + except Exception as err: + raise RemovalError(f"Error while attempting to clear the repository: {err}") diff --git a/monkey/monkey_island/cc/repository/i_agent_binary_repository.py b/monkey/monkey_island/cc/repository/i_agent_binary_repository.py index 56cf2f96b..6c9abcc40 100644 --- a/monkey/monkey_island/cc/repository/i_agent_binary_repository.py +++ b/monkey/monkey_island/cc/repository/i_agent_binary_repository.py @@ -2,10 +2,6 @@ import abc from typing import BinaryIO -class AgentRetrievalError(IOError): - pass - - class IAgentBinaryRepository(metaclass=abc.ABCMeta): """ A repository that retrieves the agent binaries @@ -17,6 +13,7 @@ class IAgentBinaryRepository(metaclass=abc.ABCMeta): Retrieve linux agent binary :return: A file-like object that represents the linux agent binary + :raises RetrievalError: If the agent binary could not be retrieved """ @abc.abstractmethod @@ -25,4 +22,5 @@ class IAgentBinaryRepository(metaclass=abc.ABCMeta): Retrieve windows agent binary :return: A file-like object that represents the windows agent binary + :raises RetrievalError: If the agent binary could not be retrieved """ diff --git a/monkey/monkey_island/cc/repository/i_agent_configuration_repository.py b/monkey/monkey_island/cc/repository/i_agent_configuration_repository.py new file mode 100644 index 000000000..ef63121c2 --- /dev/null +++ b/monkey/monkey_island/cc/repository/i_agent_configuration_repository.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod + +from common.configuration import AgentConfiguration + + +class IAgentConfigurationRepository(ABC): + """ + A repository used to store and retrieve the agent configuration. + """ + + @abstractmethod + def get_configuration(self) -> AgentConfiguration: + """ + Retrieve the agent configuration from the repository + + :return: The agent configuration as retrieved from the repository, or the default + configuration if the repository is empty + :raises RetrievalError: If the configuration could not be retrieved + """ + pass + + @abstractmethod + def store_configuration(self, agent_configuration: AgentConfiguration): + """ + Store the agent configuration in the repository + + :param agent_configuration: The agent configuration to store in the repository + :raises StorageError: If the configuration could not be stored + """ + pass diff --git a/monkey/monkey_island/cc/repository/i_config_repository.py b/monkey/monkey_island/cc/repository/i_config_repository.py deleted file mode 100644 index 70e59a89f..000000000 --- a/monkey/monkey_island/cc/repository/i_config_repository.py +++ /dev/null @@ -1,30 +0,0 @@ -from abc import ABC -from typing import Any, Mapping, Sequence - - -class IConfigRepository(ABC): - - # Config - ############################################### - - # This returns the current config - # TODO investigate if encryption should be here or where - # TODO potentially should be a DTO as well, but it's structure is defined in schema already - def get_config(self) -> Mapping: - pass - - def set_config(self, config: dict): - pass - - # Used when only a subset of config is submitted, for example only PBAFiles - # Used by passing keys, like ['monkey', 'post_breach_actions', 'linux_filename'] - # Using a list is less ambiguous IMO, than using . notation - def set_config_field(self, key_list: Sequence[str], value: Any): - pass - - # Used when only a subset of config is needed, for example only PBAFiles - # Used by passing keys, like ['monkey', 'post_breach_actions', 'linux_filename'] - # Using a list is less ambiguous IMO, than using . notation - # TODO Still in doubt about encryption, this should probably be determined automatically - def get_config_field(self, key_list: Sequence[str]) -> Any: - pass diff --git a/monkey/monkey_island/cc/resources/agent_binaries.py b/monkey/monkey_island/cc/resources/agent_binaries.py index 8d960b6a4..0d746b932 100644 --- a/monkey/monkey_island/cc/resources/agent_binaries.py +++ b/monkey/monkey_island/cc/resources/agent_binaries.py @@ -2,7 +2,7 @@ import logging from flask import make_response, send_file -from monkey_island.cc.repository import AgentRetrievalError, IAgentBinaryRepository +from monkey_island.cc.repository import IAgentBinaryRepository, RetrievalError from monkey_island.cc.resources.AbstractResource import AbstractResource logger = logging.getLogger(__name__) @@ -31,10 +31,10 @@ class AgentBinaries(AbstractResource): file = agent_binaries[os]() return send_file(file, mimetype="application/octet-stream") - except AgentRetrievalError as err: - logger.error(err) - return make_response({"error": str(err)}, 500) except KeyError as err: error_msg = f'No Agents are available for unsupported operating system "{os}": {err}' logger.error(error_msg) return make_response({"error": error_msg}, 404) + except RetrievalError as err: + logger.error(err) + return make_response({"error": str(err)}, 500) diff --git a/monkey/monkey_island/cc/resources/agent_configuration.py b/monkey/monkey_island/cc/resources/agent_configuration.py new file mode 100644 index 000000000..4715680a0 --- /dev/null +++ b/monkey/monkey_island/cc/resources/agent_configuration.py @@ -0,0 +1,35 @@ +import json + +from flask import make_response, request + +from common.configuration.agent_configuration import AgentConfiguration as AgentConfigurationObject +from common.configuration.agent_configuration 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 AgentConfiguration(AbstractResource): + urls = ["/api/agent-configuration"] + + def __init__(self, agent_configuration_repository: IAgentConfigurationRepository): + self._agent_configuration_repository = agent_configuration_repository + + @jwt_required + def get(self): + configuration = self._agent_configuration_repository.get_configuration() + configuration_json = AgentConfigurationObject.to_json(configuration) + return make_response(configuration_json, 200) + + @jwt_required + def post(self): + + try: + configuration_object = AgentConfigurationObject.from_json(request.data) + self._agent_configuration_repository.store_configuration(configuration_object) + return make_response({}, 200) + except (InvalidConfigurationError, json.JSONDecodeError) as err: + return make_response( + {"error": f"Invalid configuration supplied: {err}"}, + 400, + ) diff --git a/monkey/monkey_island/cc/resources/pba_file_download.py b/monkey/monkey_island/cc/resources/pba_file_download.py index 85395d5e3..d8359049a 100644 --- a/monkey/monkey_island/cc/resources/pba_file_download.py +++ b/monkey/monkey_island/cc/resources/pba_file_download.py @@ -2,7 +2,8 @@ import logging from flask import make_response, send_file -from monkey_island.cc.repository import FileRetrievalError, IFileRepository +from monkey_island.cc import repository +from monkey_island.cc.repository import IFileRepository from monkey_island.cc.resources.AbstractResource import AbstractResource logger = logging.getLogger(__file__) @@ -24,7 +25,6 @@ class PBAFileDownload(AbstractResource): # `send_file()` handles the closing of the open file. return send_file(file, mimetype="application/octet-stream") - except FileRetrievalError as err: - error_msg = f"Failed to open file {filename}: {err}" - logger.error(error_msg) - return make_response({"error": error_msg}, 404) + except repository.FileNotFoundError as err: + logger.error(str(err)) + return make_response({"error": str(err)}, 404) diff --git a/monkey/monkey_island/cc/resources/pba_file_upload.py b/monkey/monkey_island/cc/resources/pba_file_upload.py index 9d5cce8b1..277804a72 100644 --- a/monkey/monkey_island/cc/resources/pba_file_upload.py +++ b/monkey/monkey_island/cc/resources/pba_file_upload.py @@ -5,7 +5,8 @@ from flask import Response, make_response, request, send_file from werkzeug.utils import secure_filename as sanitize_filename from common.config_value_paths import PBA_LINUX_FILENAME_PATH, PBA_WINDOWS_FILENAME_PATH -from monkey_island.cc.repository import FileRetrievalError, IFileRepository +from monkey_island.cc import repository +from monkey_island.cc.repository import IFileRepository from monkey_island.cc.resources.AbstractResource import AbstractResource from monkey_island.cc.resources.request_authentication import jwt_required from monkey_island.cc.services.config import ConfigService @@ -57,10 +58,9 @@ class FileUpload(AbstractResource): # `send_file()` handles the closing of the open file. return send_file(file, mimetype="application/octet-stream") - except FileRetrievalError as err: - error_msg = f"Failed to open file {filename}: {err}" - logger.error(error_msg) - return make_response({"error": error_msg}, 404) + except repository.FileNotFoundError as err: + logger.error(str(err)) + return make_response({"error": str(err)}, 404) @jwt_required def post(self, target_os): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 82346258f..6d14505c3 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -3,7 +3,6 @@ import copy import functools import logging import re -from itertools import chain from typing import Any, Dict, List from jsonschema import Draft4Validator, validators @@ -180,6 +179,7 @@ class ConfigService: should_encrypt=True, ) + @staticmethod def _filter_none_values(data): if isinstance(data, dict): return { @@ -357,6 +357,7 @@ class ConfigService: ConfigService._format_payloads_from_flat_config(config) ConfigService._format_pbas_from_flat_config(config) ConfigService._format_propagation_from_flat_config(config) + ConfigService._format_credential_collectors(config) # Ok, I'll admit this is just sort of jammed in here. But this code is going away very soon. del config["HTTP_PORTS"] @@ -376,9 +377,18 @@ class ConfigService: for field in fields_to_remove: config.pop(field, None) + @staticmethod + def _format_credential_collectors(config: Dict): + collectors = [ + {"name": collector, "options": {}} for collector in config["credential_collectors"] + ] + config["credential_collectors"] = collectors + @staticmethod def _format_payloads_from_flat_config(config: Dict): - config.setdefault("payloads", {})["ransomware"] = config["ransomware"] + config.setdefault("payloads", []).append( + {"name": "ransomware", "options": config["ransomware"]} + ) config.pop("ransomware", None) @staticmethod @@ -388,9 +398,9 @@ class ConfigService: flat_windows_command_field = "custom_PBA_windows_cmd" flat_windows_filename_field = "PBA_windows_filename" - formatted_pbas_config = {} - for pba in config.get("post_breach_actions", []): - formatted_pbas_config[pba] = {} + formatted_pbas_config = [ + {"name": pba, "options": {}} for pba in config.get("post_breach_actions", []) + ] config["custom_pbas"] = { "linux_command": config.get(flat_linux_command_field, ""), @@ -408,24 +418,24 @@ class ConfigService: @staticmethod def _format_propagation_from_flat_config(config: Dict): - formatted_propagation_config = {"network_scan": {}, "targets": {}} + formatted_propagation_config = {"network_scan": {}, "maximum_depth": {}, "exploitation": {}} formatted_propagation_config[ "network_scan" ] = ConfigService._format_network_scan_from_flat_config(config) - formatted_propagation_config["targets"] = ConfigService._format_targets_from_flat_config( - config - ) formatted_propagation_config[ - "exploiters" + "exploitation" ] = ConfigService._format_exploiters_from_flat_config(config) + formatted_propagation_config["maximum_depth"] = config["depth"] + del config["depth"] + config["propagation"] = formatted_propagation_config @staticmethod def _format_network_scan_from_flat_config(config: Dict) -> Dict[str, Any]: - formatted_network_scan_config = {"tcp": {}, "icmp": {}, "fingerprinters": []} + formatted_network_scan_config = {"tcp": {}, "icmp": {}, "fingerprinters": [], "targets": {}} formatted_network_scan_config["tcp"] = ConfigService._format_tcp_scan_from_flat_config( config @@ -437,6 +447,10 @@ class ConfigService: "fingerprinters" ] = ConfigService._format_fingerprinters_from_flat_config(config) + formatted_network_scan_config["targets"] = ConfigService._format_targets_from_flat_config( + config + ) + return formatted_network_scan_config @staticmethod @@ -447,7 +461,7 @@ class ConfigService: formatted_tcp_scan_config = {} - formatted_tcp_scan_config["timeout_ms"] = config[flat_tcp_timeout_field] + formatted_tcp_scan_config["timeout"] = config[flat_tcp_timeout_field] / 1000 ports = ConfigService._union_tcp_and_http_ports( config[flat_tcp_ports_field], config[flat_http_ports_field] @@ -471,7 +485,7 @@ class ConfigService: flat_ping_timeout_field = "ping_scan_timeout" formatted_icmp_scan_config = {} - formatted_icmp_scan_config["timeout_ms"] = config[flat_ping_timeout_field] + formatted_icmp_scan_config["timeout"] = config[flat_ping_timeout_field] / 1000 config.pop(flat_ping_timeout_field, None) @@ -519,9 +533,7 @@ class ConfigService: formatted_scan_targets_config[flat_local_network_scan_field] = config[ flat_local_network_scan_field ] - formatted_scan_targets_config[flat_subnet_scan_list_field] = config[ - flat_subnet_scan_list_field - ] + formatted_scan_targets_config["subnets"] = config[flat_subnet_scan_list_field] config.pop(flat_blocked_ips_field, None) config.pop(flat_inaccessible_subnets_field, None) @@ -567,7 +579,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( @@ -580,23 +592,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": ["linux", "windows"], - "Log4ShellExploiter": ["linux", "windows"], - "MSSQLExploiter": ["windows"], - "PowerShellExploiter": ["windows"], - "SSHExploiter": ["linux"], - "SmbExploiter": ["windows"], - "WmiExploiter": ["windows"], - "ZerologonExploiter": ["windows"], - } - new_config = copy.deepcopy(formatted_config) - for exploiter in chain(new_config["brute_force"], new_config["vulnerability"]): - exploiter["supported_os"] = supported_os.get(exploiter["name"], []) - - return new_config diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index a5d283cb3..922c3654b 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -3,13 +3,16 @@ from pathlib import Path from common import DIContainer from common.aws import AWSInstance +from common.configuration import DEFAULT_AGENT_CONFIGURATION, AgentConfiguration from common.utils.file_utils import get_binary_io_sha256_hash from monkey_island.cc.repository import ( AgentBinaryRepository, - AgentRetrievalError, + FileAgentConfigurationRepository, IAgentBinaryRepository, + IAgentConfigurationRepository, IFileRepository, LocalStorageFileRepository, + RetrievalError, ) from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import AWSService @@ -28,14 +31,20 @@ def initialize_services(data_dir: Path) -> DIContainer: container = DIContainer() container.register_convention(Path, "data_dir", data_dir) + container.register_convention( + AgentConfiguration, "default_agent_configuration", DEFAULT_AGENT_CONFIGURATION + ) container.register_instance(AWSInstance, AWSInstance()) container.register_instance( - IFileRepository, LocalStorageFileRepository(data_dir / "custom_pbas") + IFileRepository, LocalStorageFileRepository(data_dir / "runtime_data") ) container.register_instance(AWSService, container.resolve(AWSService)) container.register_instance(IAgentBinaryRepository, _build_agent_binary_repository()) container.register_instance(LocalMonkeyRunService, container.resolve(LocalMonkeyRunService)) + container.register_instance( + IAgentConfigurationRepository, container.resolve(FileAgentConfigurationRepository) + ) # This is temporary until we get DI all worked out. PostBreachFilesService.initialize(container.resolve(IFileRepository)) @@ -71,7 +80,7 @@ def _log_agent_binary_hashes(agent_binary_repository: IAgentBinaryRepository): agent_binary = get_agent_binary() binary_sha256_hash = get_binary_io_sha256_hash(agent_binary) agent_hashes[os] = binary_sha256_hash - except AgentRetrievalError as err: + except RetrievalError as err: logger.error(f"No agent available for {os}: {err}") for os, binary_sha256_hash in agent_hashes.items(): diff --git a/monkey/monkey_island/cc/services/representations.py b/monkey/monkey_island/cc/services/representations.py index 0193fae0d..8a1e849a6 100644 --- a/monkey/monkey_island/cc/services/representations.py +++ b/monkey/monkey_island/cc/services/representations.py @@ -1,4 +1,5 @@ from datetime import datetime +from enum import Enum import bson from bson.json_util import dumps @@ -11,19 +12,27 @@ def normalize_obj(obj): del obj["_id"] for key, value in list(obj.items()): - if isinstance(value, bson.objectid.ObjectId): - obj[key] = str(value) - if isinstance(value, datetime): - obj[key] = str(value) - if isinstance(value, dict): - obj[key] = normalize_obj(value) if isinstance(value, list): for i in range(0, len(value)): - if isinstance(value[i], dict): - value[i] = normalize_obj(value[i]) + obj[key][i] = _normalize_value(value[i]) + else: + obj[key] = _normalize_value(value) return obj +def _normalize_value(value): + if type(value) == dict: + return normalize_obj(value) + if isinstance(value, bson.objectid.ObjectId): + return str(value) + if isinstance(value, datetime): + return str(value) + if issubclass(type(value), Enum): + return value.name + else: + return value + + def output_json(obj, code, headers=None): obj = normalize_obj(obj) resp = make_response(dumps(obj), code) diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index a54e2c6b9..a56642e2c 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -5,7 +5,7 @@ import subprocess from pathlib import Path from shutil import copyfileobj -from monkey_island.cc.repository import AgentRetrievalError, IAgentBinaryRepository +from monkey_island.cc.repository import IAgentBinaryRepository, RetrievalError from monkey_island.cc.server_utils.consts import ISLAND_PORT from monkey_island.cc.services.utils.network_utils import local_ip_addresses @@ -29,7 +29,7 @@ class LocalMonkeyRunService: } agent_binary = agents[platform.system().lower()]() - except AgentRetrievalError as err: + except RetrievalError as err: logger.error( f"No Agent can be retrieved for the specified operating system" f'"{operating_system}"' diff --git a/monkey/tests/common/example_agent_configuration.py b/monkey/tests/common/example_agent_configuration.py new file mode 100644 index 000000000..25a1dbd5e --- /dev/null +++ b/monkey/tests/common/example_agent_configuration.py @@ -0,0 +1,73 @@ +PLUGIN_NAME = "bond" +PLUGIN_OPTIONS = {"gun": "Walther PPK", "car": "Aston Martin DB5"} +PLUGIN_CONFIGURATION = {"name": PLUGIN_NAME, "options": PLUGIN_OPTIONS} + +LINUX_COMMAND = "a" +LINUX_FILENAME = "b" +WINDOWS_COMMAND = "c" +WINDOWS_FILENAME = "d" +CUSTOM_PBA_CONFIGURATION = { + "linux_command": LINUX_COMMAND, + "linux_filename": LINUX_FILENAME, + "windows_command": WINDOWS_COMMAND, + "windows_filename": WINDOWS_FILENAME, +} + +BLOCKED_IPS = ["10.0.0.1", "192.168.1.1"] +INACCESSIBLE_SUBNETS = ["172.0.0.0/24", "172.2.2.0/24", "192.168.56.0/24"] +LOCAL_NETWORK_SCAN = True +SUBNETS = ["10.0.0.2", "10.0.0.2/16"] +SCAN_TARGET_CONFIGURATION = { + "blocked_ips": BLOCKED_IPS, + "inaccessible_subnets": INACCESSIBLE_SUBNETS, + "local_network_scan": LOCAL_NETWORK_SCAN, + "subnets": SUBNETS, +} + +TIMEOUT = 2.525 +ICMP_CONFIGURATION = {"timeout": TIMEOUT} + +PORTS = [8080, 443] +TCP_SCAN_CONFIGURATION = {"timeout": TIMEOUT, "ports": PORTS} + +FINGERPRINTERS = [{"name": "mssql", "options": {}}] +NETWORK_SCAN_CONFIGURATION = { + "tcp": TCP_SCAN_CONFIGURATION, + "icmp": ICMP_CONFIGURATION, + "fingerprinters": FINGERPRINTERS, + "targets": SCAN_TARGET_CONFIGURATION, +} + +BRUTE_FORCE = [ + {"name": "ex1", "options": {}}, + { + "name": "ex2", + "options": {"smb_download_timeout": 10}, + }, +] +VULNERABILITY = [ + { + "name": "ex3", + "options": {"smb_download_timeout": 10}, + }, +] +EXPLOITATION_CONFIGURATION = { + "options": {"http_ports": PORTS}, + "brute_force": BRUTE_FORCE, + "vulnerability": VULNERABILITY, +} + +PROPAGATION_CONFIGURATION = { + "maximum_depth": 5, + "network_scan": NETWORK_SCAN_CONFIGURATION, + "exploitation": EXPLOITATION_CONFIGURATION, +} + +AGENT_CONFIGURATION = { + "keep_tunnel_open_time": 30, + "custom_pbas": CUSTOM_PBA_CONFIGURATION, + "post_breach_actions": [PLUGIN_CONFIGURATION], + "credential_collectors": [PLUGIN_CONFIGURATION], + "payloads": [PLUGIN_CONFIGURATION], + "propagation": PROPAGATION_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 deleted file mode 100644 index 7fcc2285d..000000000 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "config": { - "propagation": { - "network_scan": { - "tcp": { - "timeout_ms": 3000, - "ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 7001, - 8088, - 9200 - ] - }, - "icmp": { - "timeout_ms": 1000 - }, - "fingerprinters": [ - "SMBFinger", - "SSHFinger", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "targets": { - "blocked_ips": ["192.168.1.1", "192.168.1.100"], - "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], - "local_network_scan": true, - "subnet_scan_list": [ - "192.168.1.50", - "192.168.56.0/24", - "10.0.33.0/30", - "10.0.0.1", - "10.0.0.2" - ] - }, - "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": {}} - ], - "vulnerability": [ - {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, - {"name": "ShellShockExploiter", "supported_os": ["linux"], "options": {}}, - {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}} - ] - } - }, - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_pbas": { - "linux_command": "", - "windows_command": "" - }, - "depth": 2, - "exploit_lm_hash_list": ["DEADBEEF", "FACADE"], - "exploit_ntlm_hash_list": ["BEADED", "ACCEDE", "DECADE"], - "exploit_password_list": ["p1", "p2", "p3"], - "exploit_ssh_keys": "hidden", - "exploit_user_list": ["u1", "u2", "u3"], - "exploiter_classes": [], - "max_depth": 2, - "post_breach_actions": { - "CommunicateAsBackdoorUser": {}, - "ModifyShellStartupFiles": {}, - "HiddenFiles": {}, - "TrapCommand": {}, - "ChangeSetuidSetgid": {}, - "ScheduleJobs": {}, - "Timestomping": {}, - "AccountDiscovery": {}, - "Custom": { - "linux_command": "chmod u+x my_exec && ./my_exec", - "windows_cmd": "powershell test_driver.ps1", - "linux_filename": "my_exec", - "windows_filename": "test_driver.ps1" - } - }, - "payloads": { - "ransomware": { - "encryption": { - "directories": {"linux_target_dir": "", "windows_target_dir": ""}, - "enabled": true - }, - "other_behaviors": {"readme": true} - } - }, - "credential_collectors": [ - "MimikatzCollector", - "SSHCollector" - ] - } -} diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 33bf50da1..42568404a 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -9,7 +9,6 @@ ], "PBA_linux_filename": "test.sh", "PBA_windows_filename": "test.ps1", - "alive": true, "blocked_ips": ["192.168.1.1", "192.168.1.100"], "custom_PBA_linux_cmd": "bash test.sh", "custom_PBA_windows_cmd": "powershell test.ps1", @@ -27,6 +26,7 @@ "private_key": "my_private_key" } ], + "credential_collectors": ["MimikatzCollector", "SSHCollector"], "exploit_user_list": [ "Administrator", "root", @@ -53,7 +53,6 @@ "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], "keep_tunnel_open_time": 60, "local_network_scan": true, - "max_depth": null, "ping_scan_timeout": 1000, "post_breach_actions": [ "CommunicateAsBackdoorUser", @@ -75,9 +74,6 @@ } }, "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], - "system_info_collector_classes": [ - "MimikatzCollector" - ], "tcp_scan_timeout": 3000, "tcp_target_ports": [ 22, diff --git a/monkey/tests/monkey_island/__init__.py b/monkey/tests/monkey_island/__init__.py new file mode 100644 index 000000000..8f3654a6e --- /dev/null +++ b/monkey/tests/monkey_island/__init__.py @@ -0,0 +1,4 @@ +from .single_file_repository import SingleFileRepository +from .mock_file_repository import MockFileRepository, FILE_CONTENTS, FILE_NAME +from .open_error_file_repository import OpenErrorFileRepository +from .in_memory_agent_configuration_repository import InMemoryAgentConfigurationRepository diff --git a/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py b/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py new file mode 100644 index 000000000..e9bcbae62 --- /dev/null +++ b/monkey/tests/monkey_island/in_memory_agent_configuration_repository.py @@ -0,0 +1,15 @@ +from tests.common.example_agent_configuration import AGENT_CONFIGURATION + +from common.configuration.agent_configuration import AgentConfiguration +from monkey_island.cc.repository import IAgentConfigurationRepository + + +class InMemoryAgentConfigurationRepository(IAgentConfigurationRepository): + def __init__(self): + self._configuration = AgentConfiguration.from_mapping(AGENT_CONFIGURATION) + + def get_configuration(self): + return self._configuration + + def store_configuration(self, agent_configuration): + self._configuration = agent_configuration diff --git a/monkey/tests/monkey_island/mock_file_repository.py b/monkey/tests/monkey_island/mock_file_repository.py new file mode 100644 index 000000000..782c9838b --- /dev/null +++ b/monkey/tests/monkey_island/mock_file_repository.py @@ -0,0 +1,28 @@ +import io +from typing import BinaryIO + +from monkey_island.cc import repository +from monkey_island.cc.repository import IFileRepository + +FILE_NAME = "test_file" +FILE_CONTENTS = b"HelloWorld!" + + +class MockFileRepository(IFileRepository): + def __init__(self): + self._file = io.BytesIO(FILE_CONTENTS) + + def save_file(self, unsafe_file_name: str, file_contents: BinaryIO): + pass + + def open_file(self, unsafe_file_name: str) -> BinaryIO: + if unsafe_file_name != FILE_NAME: + raise repository.FileNotFoundError() + + return self._file + + def delete_file(self, unsafe_file_name: str): + pass + + def delete_all_files(self): + pass diff --git a/monkey/tests/monkey_island/open_error_file_repository.py b/monkey/tests/monkey_island/open_error_file_repository.py new file mode 100644 index 000000000..c13559613 --- /dev/null +++ b/monkey/tests/monkey_island/open_error_file_repository.py @@ -0,0 +1,10 @@ +from typing import BinaryIO + +from monkey_island.cc.repository import RetrievalError + +from . import MockFileRepository + + +class OpenErrorFileRepository(MockFileRepository): + def open_file(self, unsafe_file_name: str) -> BinaryIO: + raise RetrievalError("Error retrieving file") diff --git a/monkey/tests/monkey_island/single_file_repository.py b/monkey/tests/monkey_island/single_file_repository.py new file mode 100644 index 000000000..462969acb --- /dev/null +++ b/monkey/tests/monkey_island/single_file_repository.py @@ -0,0 +1,24 @@ +import io +from typing import BinaryIO + +from monkey_island.cc import repository +from monkey_island.cc.repository import IFileRepository + + +class SingleFileRepository(IFileRepository): + def __init__(self): + self._file = None + + def save_file(self, unsafe_file_name: str, file_contents: BinaryIO): + self._file = io.BytesIO(file_contents.read()) + + def open_file(self, unsafe_file_name: str) -> BinaryIO: + if self._file is None: + raise repository.FileNotFoundError() + return self._file + + def delete_file(self, unsafe_file_name: str): + self._file = None + + def delete_all_files(self): + self.delete_file("") diff --git a/monkey/tests/unit_tests/common/configuration/test_agent_configuration.py b/monkey/tests/unit_tests/common/configuration/test_agent_configuration.py new file mode 100644 index 000000000..fecb6a6f6 --- /dev/null +++ b/monkey/tests/unit_tests/common/configuration/test_agent_configuration.py @@ -0,0 +1,219 @@ +import json +from copy import deepcopy + +import pytest +from tests.common.example_agent_configuration import ( + AGENT_CONFIGURATION, + BLOCKED_IPS, + CUSTOM_PBA_CONFIGURATION, + EXPLOITATION_CONFIGURATION, + FINGERPRINTERS, + ICMP_CONFIGURATION, + INACCESSIBLE_SUBNETS, + LINUX_COMMAND, + LINUX_FILENAME, + LOCAL_NETWORK_SCAN, + NETWORK_SCAN_CONFIGURATION, + PLUGIN_CONFIGURATION, + PLUGIN_NAME, + PLUGIN_OPTIONS, + PORTS, + PROPAGATION_CONFIGURATION, + SCAN_TARGET_CONFIGURATION, + SUBNETS, + TCP_SCAN_CONFIGURATION, + TIMEOUT, + WINDOWS_COMMAND, + WINDOWS_FILENAME, +) + +from common.configuration import AgentConfiguration, InvalidConfigurationError +from common.configuration.agent_configuration import AgentConfigurationSchema +from common.configuration.agent_sub_configuration_schemas import ( + CustomPBAConfigurationSchema, + ExploitationConfigurationSchema, + ExploitationOptionsConfigurationSchema, + ICMPScanConfigurationSchema, + NetworkScanConfigurationSchema, + PluginConfigurationSchema, + PropagationConfigurationSchema, + ScanTargetConfigurationSchema, + TCPScanConfigurationSchema, +) +from common.configuration.agent_sub_configurations import ( + CustomPBAConfiguration, + ExploitationConfiguration, + NetworkScanConfiguration, + PluginConfiguration, + PropagationConfiguration, +) + + +def test_build_plugin_configuration(): + schema = PluginConfigurationSchema() + + config = schema.load(PLUGIN_CONFIGURATION) + + assert config.name == PLUGIN_NAME + assert config.options == PLUGIN_OPTIONS + + +def test_custom_pba_configuration_schema(): + schema = CustomPBAConfigurationSchema() + + config = schema.load(CUSTOM_PBA_CONFIGURATION) + + assert config.linux_command == LINUX_COMMAND + assert config.linux_filename == LINUX_FILENAME + assert config.windows_command == WINDOWS_COMMAND + assert config.windows_filename == WINDOWS_FILENAME + + +def test_scan_target_configuration(): + schema = ScanTargetConfigurationSchema() + + config = schema.load(SCAN_TARGET_CONFIGURATION) + + assert config.blocked_ips == BLOCKED_IPS + assert config.inaccessible_subnets == INACCESSIBLE_SUBNETS + assert config.local_network_scan == LOCAL_NETWORK_SCAN + assert config.subnets == SUBNETS + + +def test_icmp_scan_configuration_schema(): + schema = ICMPScanConfigurationSchema() + + config = schema.load(ICMP_CONFIGURATION) + + assert config.timeout == TIMEOUT + + +def test_tcp_scan_configuration_schema(): + schema = TCPScanConfigurationSchema() + + config = schema.load(TCP_SCAN_CONFIGURATION) + + assert config.timeout == TIMEOUT + assert config.ports == PORTS + + +def test_network_scan_configuration(): + schema = NetworkScanConfigurationSchema() + + config = schema.load(NETWORK_SCAN_CONFIGURATION) + + assert config.tcp.ports == TCP_SCAN_CONFIGURATION["ports"] + assert config.tcp.timeout == TCP_SCAN_CONFIGURATION["timeout"] + assert config.icmp.timeout == ICMP_CONFIGURATION["timeout"] + assert config.fingerprinters[0].name == FINGERPRINTERS[0]["name"] + assert config.fingerprinters[0].options == FINGERPRINTERS[0]["options"] + assert config.targets.blocked_ips == BLOCKED_IPS + assert config.targets.inaccessible_subnets == INACCESSIBLE_SUBNETS + assert config.targets.local_network_scan == LOCAL_NETWORK_SCAN + assert config.targets.subnets == SUBNETS + + +def test_exploitation_options_configuration_schema(): + ports = [1, 2, 3] + schema = ExploitationOptionsConfigurationSchema() + + config = schema.load({"http_ports": ports}) + + assert config.http_ports == ports + + +def test_exploiter_configuration_schema(): + name = "bond" + options = {"gun": "Walther PPK", "car": "Aston Martin DB5"} + schema = PluginConfigurationSchema() + + config = schema.load({"name": name, "options": options}) + + assert config.name == name + assert config.options == options + + +def test_exploitation_configuration(): + schema = ExploitationConfigurationSchema() + + config = schema.load(EXPLOITATION_CONFIGURATION) + config_dict = schema.dump(config) + + assert isinstance(config, ExploitationConfiguration) + assert config_dict == EXPLOITATION_CONFIGURATION + + +def test_propagation_configuration(): + schema = PropagationConfigurationSchema() + + config = schema.load(PROPAGATION_CONFIGURATION) + config_dict = schema.dump(config) + + assert isinstance(config, PropagationConfiguration) + assert isinstance(config.network_scan, NetworkScanConfiguration) + assert isinstance(config.exploitation, ExploitationConfiguration) + assert config.maximum_depth == 5 + assert config_dict == PROPAGATION_CONFIGURATION + + +def test_agent_configuration(): + config = AgentConfiguration.from_mapping(AGENT_CONFIGURATION) + config_json = AgentConfiguration.to_json(config) + + assert isinstance(config, AgentConfiguration) + assert config.keep_tunnel_open_time == 30 + assert isinstance(config.custom_pbas, CustomPBAConfiguration) + assert isinstance(config.post_breach_actions[0], PluginConfiguration) + assert isinstance(config.credential_collectors[0], PluginConfiguration) + assert isinstance(config.payloads[0], PluginConfiguration) + assert isinstance(config.propagation, PropagationConfiguration) + assert json.loads(config_json) == AGENT_CONFIGURATION + + +def test_incorrect_type(): + valid_config = AgentConfiguration.from_mapping(AGENT_CONFIGURATION) + with pytest.raises(InvalidConfigurationError): + valid_config_dict = valid_config.__dict__ + valid_config_dict["keep_tunnel_open_time"] = "not_a_float" + AgentConfiguration(**valid_config_dict) + + +def test_from_dict(): + schema = AgentConfigurationSchema() + dict_ = deepcopy(AGENT_CONFIGURATION) + + config = AgentConfiguration.from_mapping(dict_) + + assert schema.dump(config) == dict_ + + +def test_from_dict__invalid_data(): + dict_ = deepcopy(AGENT_CONFIGURATION) + dict_["payloads"] = "payloads" + + with pytest.raises(InvalidConfigurationError): + AgentConfiguration.from_mapping(dict_) + + +def test_from_json(): + schema = AgentConfigurationSchema() + dict_ = deepcopy(AGENT_CONFIGURATION) + + config = AgentConfiguration.from_json(json.dumps(dict_)) + + assert isinstance(config, AgentConfiguration) + assert schema.dump(config) == dict_ + + +def test_from_json__invalid_data(): + invalid_dict = deepcopy(AGENT_CONFIGURATION) + invalid_dict["payloads"] = "payloads" + + with pytest.raises(InvalidConfigurationError): + AgentConfiguration.from_json(json.dumps(invalid_dict)) + + +def test_to_json(): + config = deepcopy(AGENT_CONFIGURATION) + + assert json.loads(AgentConfiguration.to_json(config)) == AGENT_CONFIGURATION diff --git a/monkey/tests/unit_tests/conftest.py b/monkey/tests/unit_tests/conftest.py index 5b83a7e69..51528ba00 100644 --- a/monkey/tests/unit_tests/conftest.py +++ b/monkey/tests/unit_tests/conftest.py @@ -9,6 +9,8 @@ from _pytest.monkeypatch import MonkeyPatch MONKEY_BASE_PATH = str(Path(__file__).parent.parent.parent) sys.path.insert(0, MONKEY_BASE_PATH) +from common.configuration import DEFAULT_AGENT_CONFIGURATION, AgentConfiguration # noqa: E402 + @pytest.fixture(scope="session") def data_for_tests_dir(pytestconfig): @@ -54,3 +56,8 @@ def load_monkey_config(data_for_tests_dir) -> Callable[[str], Dict]: 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/infection_monkey/conftest.py b/monkey/tests/unit_tests/infection_monkey/conftest.py index 14a193112..533572f98 100644 --- a/monkey/tests/unit_tests/infection_monkey/conftest.py +++ b/monkey/tests/unit_tests/infection_monkey/conftest.py @@ -15,8 +15,3 @@ class TelemetryMessengerSpy(ITelemetryMessenger): @pytest.fixture def telemetry_messenger_spy(): return TelemetryMessengerSpy() - - -@pytest.fixture -def automated_master_config(load_monkey_config): - return load_monkey_config("automated_master_config.json") diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py index a339ec17f..e99385c5b 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py @@ -2,6 +2,7 @@ from unittest.mock import Mock import pytest +from common import OperatingSystems from infection_monkey.exploit.tools.helpers import ( AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64, @@ -13,22 +14,28 @@ from infection_monkey.exploit.tools.helpers import ( def _get_host(os): host = Mock() host.os = {"type": os} + host.is_windows = lambda: os == OperatingSystems.WINDOWS return host @pytest.mark.parametrize( - "os, path", [("linux", AGENT_BINARY_PATH_LINUX), ("windows", AGENT_BINARY_PATH_WIN64)] + "os, path", + [ + (OperatingSystems.LINUX, AGENT_BINARY_PATH_LINUX), + (OperatingSystems.WINDOWS, AGENT_BINARY_PATH_WIN64), + ], ) def test_get_agent_dst_path(os, path): host = _get_host(os) rand_path = get_agent_dst_path(host) + print(f"{os}: {rand_path}") # Assert that filename got longer by RAND_SUFFIX_LEN and one dash assert len(str(rand_path)) == (len(str(path)) + RAND_SUFFIX_LEN + 1) def test_get_agent_dst_path_randomness(): - host = _get_host("windows") + host = _get_host(OperatingSystems.WINDOWS) path1 = get_agent_dst_path(host) path2 = get_agent_dst_path(host) @@ -37,7 +44,7 @@ def test_get_agent_dst_path_randomness(): def test_get_agent_dst_path_str_place(): - host = _get_host("windows") + host = _get_host(OperatingSystems.WINDOWS) rand_path = get_agent_dst_path(host) diff --git a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py index 295c68ceb..4d77bd0b3 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -2,6 +2,7 @@ import logging import threading from typing import Dict, Iterable, List, Sequence +from common import OperatingSystems from infection_monkey.credential_collectors import LMHash, Password, SSHKeypair, Username from infection_monkey.i_puppet import ( Credentials, @@ -182,19 +183,23 @@ class MockPuppet(IPuppet): "vulnerable_ports": [22], "executed_cmds": [], } - os_windows = "windows" - os_linux = "linux" successful_exploiters = { DOT_1: { "ZerologonExploiter": ExploiterResultData( - False, False, False, os_windows, {}, [], "Zerologon failed" + False, False, False, OperatingSystems.WINDOWS, {}, [], "Zerologon failed" ), "SSHExploiter": ExploiterResultData( - False, False, False, os_linux, info_ssh, attempts, "Failed exploiting" + False, + False, + False, + OperatingSystems.LINUX, + info_ssh, + attempts, + "Failed exploiting", ), "WmiExploiter": ExploiterResultData( - True, True, False, os_windows, info_wmi, attempts, None + True, True, False, OperatingSystems.WINDOWS, info_wmi, attempts, None ), }, DOT_3: { @@ -202,16 +207,22 @@ class MockPuppet(IPuppet): False, False, False, - os_windows, + OperatingSystems.WINDOWS, info_wmi, attempts, "PowerShell Exploiter Failed", ), "SSHExploiter": ExploiterResultData( - False, False, False, os_linux, info_ssh, attempts, "Failed exploiting" + False, + False, + False, + OperatingSystems.LINUX, + info_ssh, + attempts, + "Failed exploiting", ), "ZerologonExploiter": ExploiterResultData( - True, False, False, os_windows, {}, [], None + True, False, False, OperatingSystems.WINDOWS, {}, [], None ), }, } @@ -220,7 +231,13 @@ class MockPuppet(IPuppet): return successful_exploiters[host.ip_addr][name] except KeyError: return ExploiterResultData( - False, False, False, os_linux, {}, [], f"{name} failed for host {host}" + False, + False, + False, + OperatingSystems.LINUX, + {}, + [], + f"{name} failed for host {host}", ) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index cf0112d59..9029ce480 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -41,14 +41,14 @@ def test_stop_if_cant_get_config_from_island(monkeypatch): @pytest.fixture -def sleep_and_return_config(automated_master_config): +def sleep_and_return_config(default_agent_configuration): # Ensure that should_agent_stop times out before get_config() returns to prevent the # Propagator's sub-threads from hanging get_config_sleep_time = INTERVAL * (CHECK_FOR_STOP_AGENT_COUNT + 1) def _inner(): time.sleep(get_config_sleep_time) - return automated_master_config + return default_agent_configuration return _inner 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..4a44cca95 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -7,6 +7,11 @@ from unittest.mock import MagicMock import pytest from tests.unit_tests.infection_monkey.master.mock_puppet import MockPuppet +from common import OperatingSystems +from common.configuration.agent_sub_configurations import ( + ExploitationConfiguration, + PluginConfiguration, +) from infection_monkey.master import Exploiter from infection_monkey.model import VictimHost @@ -34,18 +39,18 @@ def callback(): @pytest.fixture -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}}, - ], - "vulnerability": [ - {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}, - ], - } +def exploiter_config(default_agent_configuration): + brute_force = [ + PluginConfiguration(name="MSSQLExploiter", options={"timeout": 10}), + PluginConfiguration(name="SSHExploiter", options={}), + PluginConfiguration(name="WmiExploiter", options={"timeout": 10}), + ] + vulnerability = [PluginConfiguration(name="ZerologonExploiter", options={})] + return ExploitationConfiguration( + options=default_agent_configuration.propagation.exploitation.options, + brute_force=brute_force, + vulnerability=vulnerability, + ) @pytest.fixture @@ -105,11 +110,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 @@ -160,7 +165,7 @@ def test_exploiter_raises_exception(callback, hosts, hosts_to_exploit, run_explo def test_windows_exploiters_run_on_windows_host(callback, hosts, hosts_to_exploit, run_exploiters): host = VictimHost("10.0.0.1") - host.os["type"] = "windows" + host.os["type"] = OperatingSystems.WINDOWS q = enqueue_hosts([host]) run_exploiters(MockPuppet(), 1, q) @@ -172,7 +177,7 @@ def test_windows_exploiters_run_on_windows_host(callback, hosts, hosts_to_exploi def test_linux_exploiters_run_on_linux_host(callback, hosts, hosts_to_exploit, run_exploiters): host = VictimHost("10.0.0.1") - host.os["type"] = "linux" + host.os["type"] = OperatingSystems.LINUX q = enqueue_hosts([host]) run_exploiters(MockPuppet(), 1, q) @@ -196,6 +201,6 @@ def test_all_exploiters_run_on_unknown_host(callback, hosts, hosts_to_exploit, r host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list) assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos - assert ("HadoopExploiter", hosts[0]) in host_exploit_combos + assert ("MSSQLExploiter", hosts[0]) in host_exploit_combos assert ("SSHExploiter", host) in host_exploit_combos assert ("WmiExploiter", hosts[0]) in host_exploit_combos diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py index 9fafdaee2..bf026510f 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py @@ -5,6 +5,12 @@ from unittest.mock import MagicMock import pytest from tests.unit_tests.infection_monkey.master.mock_puppet import MockPuppet +from common.configuration.agent_sub_configurations import ( + ICMPScanConfiguration, + NetworkScanConfiguration, + PluginConfiguration, + TCPScanConfiguration, +) from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus from infection_monkey.master import IPScanner from infection_monkey.network import NetworkAddress @@ -14,28 +20,31 @@ LINUX_OS = "linux" @pytest.fixture -def scan_config(): - return { - "tcp": { - "timeout_ms": 3000, - "ports": [ - 22, - 445, - 3389, - 443, - 8008, - 3306, - ], - }, - "icmp": { - "timeout_ms": 1000, - }, - "fingerprinters": [ - {"name": "HTTPFinger", "options": {}}, - {"name": "SMBFinger", "options": {}}, - {"name": "SSHFinger", "options": {}}, +def scan_config(default_agent_configuration): + tcp_config = TCPScanConfiguration( + timeout=3, + ports=[ + 22, + 445, + 3389, + 443, + 8008, + 3306, ], - } + ) + icmp_config = ICMPScanConfiguration(timeout=1) + fingerprinter_config = [ + PluginConfiguration(name="HTTPFinger", options={}), + PluginConfiguration(name="SMBFinger", options={}), + PluginConfiguration(name="SSHFinger", options={}), + ] + scan_config = NetworkScanConfiguration( + tcp_config, + icmp_config, + fingerprinter_config, + default_agent_configuration.propagation.network_scan.targets, + ) + return scan_config @pytest.fixture diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py index 3746e65eb..2ebdcd84e 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -3,6 +3,11 @@ from unittest.mock import MagicMock import pytest +from common.configuration.agent_sub_configurations import ( + NetworkScanConfiguration, + PropagationConfiguration, + ScanTargetConfiguration, +) from infection_monkey.i_puppet import ( ExploiterResultData, FingerprintData, @@ -135,24 +140,37 @@ class StubExploiter: pass -def test_scan_result_processing(telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory): +def get_propagation_config( + default_agent_configuration, scan_target_config: ScanTargetConfiguration +): + network_scan = NetworkScanConfiguration( + default_agent_configuration.propagation.network_scan.tcp, + default_agent_configuration.propagation.network_scan.icmp, + default_agent_configuration.propagation.network_scan.fingerprinters, + scan_target_config, + ) + propagation_config = PropagationConfiguration( + default_agent_configuration.propagation.maximum_depth, + network_scan, + default_agent_configuration.propagation.exploitation, + ) + return propagation_config + + +def test_scan_result_processing( + telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory, default_agent_configuration +): p = Propagator( telemetry_messenger_spy, mock_ip_scanner, StubExploiter(), mock_victim_host_factory, [] ) - p.propagate( - { - "targets": { - "subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"], - "local_network_scan": False, - "inaccessible_subnets": [], - "blocked_ips": [], - }, - "network_scan": {}, # This is empty since MockIPscanner ignores it - "exploiters": {}, # This is empty since StubExploiter ignores it - }, - 1, - Event(), + targets = ScanTargetConfiguration( + blocked_ips=[], + inaccessible_subnets=[], + local_network_scan=False, + subnets=["10.0.0.1", "10.0.0.2", "10.0.0.3"], ) + propagation_config = get_propagation_config(default_agent_configuration, targets) + p.propagate(propagation_config, 1, Event()) assert len(telemetry_messenger_spy.telemetries) == 3 @@ -237,25 +255,20 @@ class MockExploiter: def test_exploiter_result_processing( - telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory + telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory, default_agent_configuration ): p = Propagator( telemetry_messenger_spy, mock_ip_scanner, MockExploiter(), mock_victim_host_factory, [] ) - p.propagate( - { - "targets": { - "subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"], - "local_network_scan": False, - "inaccessible_subnets": [], - "blocked_ips": [], - }, - "network_scan": {}, # This is empty since MockIPscanner ignores it - "exploiters": {}, # This is empty since MockExploiter ignores it - }, - 1, - Event(), + + targets = ScanTargetConfiguration( + blocked_ips=[], + inaccessible_subnets=[], + local_network_scan=False, + subnets=["10.0.0.1", "10.0.0.2", "10.0.0.3"], ) + propagation_config = get_propagation_config(default_agent_configuration, targets) + p.propagate(propagation_config, 1, Event()) exploit_telems = [t for t in telemetry_messenger_spy.telemetries if isinstance(t, ExploitTelem)] assert len(exploit_telems) == 4 @@ -278,7 +291,9 @@ def test_exploiter_result_processing( assert data["propagation_result"] -def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory): +def test_scan_target_generation( + telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory, default_agent_configuration +): local_network_interfaces = [NetworkInterface("10.0.0.9", "/29")] p = Propagator( telemetry_messenger_spy, @@ -287,20 +302,15 @@ def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner, mock_v mock_victim_host_factory, local_network_interfaces, ) - p.propagate( - { - "targets": { - "subnet_scan_list": ["10.0.0.0/29", "172.10.20.30"], - "local_network_scan": True, - "blocked_ips": ["10.0.0.3"], - "inaccessible_subnets": ["10.0.0.128/30", "10.0.0.8/29"], - }, - "network_scan": {}, # This is empty since MockIPscanner ignores it - "exploiters": {}, # This is empty since MockExploiter ignores it - }, - 1, - Event(), + targets = ScanTargetConfiguration( + blocked_ips=["10.0.0.3"], + inaccessible_subnets=["10.0.0.128/30", "10.0.0.8/29"], + local_network_scan=True, + subnets=["10.0.0.0/29", "172.10.20.30"], ) + propagation_config = get_propagation_config(default_agent_configuration, targets) + p.propagate(propagation_config, 1, Event()) + expected_ip_scan_list = [ "10.0.0.0", "10.0.0.1", diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping_scanner.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping_scanner.py index 88c9dbeca..9e2891e3f 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping_scanner.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock import pytest +from common import OperatingSystems from infection_monkey.network_scanning import ping from infection_monkey.network_scanning.ping_scanner import EMPTY_PING_SCAN @@ -91,7 +92,7 @@ def test_linux_ping_success(patch_subprocess_running_ping_with_ping_output): result = ping("192.168.1.1", 1.0) assert result.response_received - assert result.os == "linux" + assert result.os == OperatingSystems.LINUX @pytest.mark.usefixtures("set_os_linux") @@ -109,7 +110,7 @@ def test_windows_ping_success(patch_subprocess_running_ping_with_ping_output): result = ping("192.168.1.1", 1.0) assert result.response_received - assert result.os == "windows" + assert result.os == OperatingSystems.WINDOWS @pytest.mark.usefixtures("set_os_windows") diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py index 69c8eb580..09d0705ef 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py @@ -1,5 +1,6 @@ import pytest +from common import OperatingSystems from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter @@ -56,7 +57,7 @@ def test_ssh_os(ssh_fingerprinter): results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) assert results == FingerprintData( - "linux", + OperatingSystems.LINUX, "Ubuntu-4ubuntu0.2", { "tcp-22": { @@ -78,7 +79,7 @@ def test_multiple_os(ssh_fingerprinter): results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) assert results == FingerprintData( - "linux", + OperatingSystems.LINUX, "Debian", { "tcp-22": { diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_propagation.py b/monkey/tests/unit_tests/infection_monkey/utils/test_propagation.py index 37f7194a6..19b2c18b5 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_propagation.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_propagation.py @@ -1,32 +1,22 @@ -from infection_monkey.utils.propagation import should_propagate +from infection_monkey.utils.propagation import maximum_depth_reached -def get_config(max_depth): - return {"config": {"depth": max_depth}} - - -def test_should_propagate_current_less_than_max(): - max_depth = 2 +def test_maximum_depth_reached__current_less_than_max(): + maximum_depth = 2 current_depth = 1 - config = get_config(max_depth) - - assert should_propagate(config, current_depth) is True + assert maximum_depth_reached(maximum_depth, current_depth) is True -def test_should_propagate_current_greater_than_max(): - max_depth = 2 +def test_maximum_depth_reached__current_greater_than_max(): + maximum_depth = 2 current_depth = 3 - config = get_config(max_depth) - - assert should_propagate(config, current_depth) is False + assert maximum_depth_reached(maximum_depth, current_depth) is False -def test_should_propagate_current_equal_to_max(): - max_depth = 2 - current_depth = max_depth +def test_maximum_depth_reached__current_equal_to_max(): + maximum_depth = 2 + current_depth = maximum_depth - config = get_config(max_depth) - - assert should_propagate(config, current_depth) is False + assert maximum_depth_reached(maximum_depth, current_depth) is False diff --git a/monkey/tests/unit_tests/monkey_island/cc/repository/test_agent_binary_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repository/test_agent_binary_repository.py new file mode 100644 index 000000000..585867b2e --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/repository/test_agent_binary_repository.py @@ -0,0 +1,31 @@ +from unittest.mock import MagicMock + +import pytest + +from monkey_island.cc.repository import AgentBinaryRepository, IFileRepository, RetrievalError + +LINUX_AGENT_BINARY = b"linux_binary" +WINDOWS_AGENT_BINARY = b"windows_binary" + + +@pytest.fixture +def error_mock_file_repository(): + mock_file_repository = MagicMock(wraps=IFileRepository) + mock_file_repository.open_file = MagicMock(side_effect=OSError) + + return mock_file_repository + + +@pytest.fixture +def agent_binary_repository(error_mock_file_repository): + return AgentBinaryRepository(error_mock_file_repository) + + +def test_get_linux_binary_retrieval_error(agent_binary_repository): + with pytest.raises(RetrievalError): + agent_binary_repository.get_linux_binary() + + +def test_get_windows_binary_retrieval_error(agent_binary_repository): + with pytest.raises(RetrievalError): + agent_binary_repository.get_windows_binary() diff --git a/monkey/tests/unit_tests/monkey_island/cc/repository/test_file_agent_configuration_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repository/test_file_agent_configuration_repository.py new file mode 100644 index 000000000..fb7863dc3 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/repository/test_file_agent_configuration_repository.py @@ -0,0 +1,35 @@ +import pytest +from tests.common.example_agent_configuration import AGENT_CONFIGURATION +from tests.monkey_island import OpenErrorFileRepository, SingleFileRepository + +from common.configuration import AgentConfiguration +from monkey_island.cc.repository import FileAgentConfigurationRepository, RetrievalError + + +@pytest.fixture +def repository(default_agent_configuration): + return FileAgentConfigurationRepository(default_agent_configuration, SingleFileRepository()) + + +def test_store_agent_config(repository): + agent_configuration = AgentConfiguration.from_mapping(AGENT_CONFIGURATION) + + repository.store_configuration(agent_configuration) + retrieved_agent_configuration = repository.get_configuration() + + assert retrieved_agent_configuration == agent_configuration + + +def test_get_default_agent_config(repository, default_agent_configuration): + retrieved_agent_configuration = repository.get_configuration() + + assert retrieved_agent_configuration == default_agent_configuration + + +def test_get_agent_config_retrieval_error(default_agent_configuration): + repository = FileAgentConfigurationRepository( + default_agent_configuration, OpenErrorFileRepository() + ) + + with pytest.raises(RetrievalError): + repository.get_configuration() diff --git a/monkey/tests/unit_tests/monkey_island/cc/repository/test_local_storage_file_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repository/test_local_storage_file_repository.py index cf506ffd0..64541bcbe 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/repository/test_local_storage_file_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/repository/test_local_storage_file_repository.py @@ -1,10 +1,12 @@ import io from pathlib import Path +from unittest.mock import Mock, patch import pytest from tests.monkey_island.utils import assert_linux_permissions, assert_windows_permissions -from monkey_island.cc.repository import FileRetrievalError, LocalStorageFileRepository +from monkey_island.cc import repository +from monkey_island.cc.repository import LocalStorageFileRepository from monkey_island.cc.server_utils.file_utils import is_windows_os @@ -131,5 +133,13 @@ def test_remove_nonexistant_file(tmp_path): def test_open_nonexistant_file(tmp_path): fss = LocalStorageFileRepository(tmp_path) - with pytest.raises(FileRetrievalError): + with pytest.raises(repository.FileNotFoundError): fss.open_file("nonexistant_file.txt") + + +def test_open_locked_file(tmp_path, monkeypatch): + fss = LocalStorageFileRepository(tmp_path) + + with patch("builtins.open", Mock(side_effect=Exception())): + with pytest.raises(repository.RetrievalError): + fss.open_file("locked_file.txt") diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/__init__.py b/monkey/tests/unit_tests/monkey_island/cc/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/conftest.py index a40766d5e..26fe24821 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/conftest.py @@ -2,11 +2,14 @@ from unittest.mock import MagicMock import flask_jwt_extended import pytest +from tests.common import StubDIContainer +from tests.monkey_island import OpenErrorFileRepository from tests.unit_tests.monkey_island.conftest import init_mock_app import monkey_island.cc.app import monkey_island.cc.resources.auth.auth import monkey_island.cc.resources.island_mode +from monkey_island.cc.repository import IFileRepository @pytest.fixture @@ -38,3 +41,12 @@ def get_mock_app(container): flask_jwt_extended.JWTManager(app) return app + + +@pytest.fixture +def open_error_flask_client(build_flask_client): + container = StubDIContainer() + container.register(IFileRepository, OpenErrorFileRepository) + + with build_flask_client(container) as flask_client: + yield flask_client diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_configuration.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_configuration.py new file mode 100644 index 000000000..874500b81 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_configuration.py @@ -0,0 +1,50 @@ +import json + +import pytest +from tests.common import StubDIContainer +from tests.common.example_agent_configuration import AGENT_CONFIGURATION +from tests.monkey_island import InMemoryAgentConfigurationRepository +from tests.unit_tests.monkey_island.conftest import get_url_for_resource + +from monkey_island.cc.repository import IAgentConfigurationRepository +from monkey_island.cc.resources.agent_configuration import AgentConfiguration + + +@pytest.fixture +def flask_client(build_flask_client): + container = StubDIContainer() + + container.register(IAgentConfigurationRepository, InMemoryAgentConfigurationRepository) + + with build_flask_client(container) as flask_client: + yield flask_client + + +def test_agent_configuration_endpoint(flask_client): + agent_configuration_url = get_url_for_resource(AgentConfiguration) + + flask_client.post( + agent_configuration_url, data=json.dumps(AGENT_CONFIGURATION), follow_redirects=True + ) + resp = flask_client.get(agent_configuration_url) + + assert resp.status_code == 200 + assert json.loads(resp.data) == AGENT_CONFIGURATION + + +def test_agent_configuration_invalid_config(flask_client): + agent_configuration_url = get_url_for_resource(AgentConfiguration) + + resp = flask_client.post( + agent_configuration_url, data=json.dumps({"invalid_config": "invalid_stuff"}) + ) + + assert resp.status_code == 400 + + +def test_agent_configuration_invalid_json(flask_client): + agent_configuration_url = get_url_for_resource(AgentConfiguration) + + resp = flask_client.post(agent_configuration_url, data="InvalidJson!") + + assert resp.status_code == 400 diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_download.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_download.py index cf2a109b9..31ae0309a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_download.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_download.py @@ -1,36 +1,11 @@ -import io -from typing import BinaryIO - import pytest from tests.common import StubDIContainer +from tests.monkey_island import FILE_CONTENTS, FILE_NAME, MockFileRepository from tests.unit_tests.monkey_island.conftest import get_url_for_resource -from monkey_island.cc.repository import FileRetrievalError, IFileRepository +from monkey_island.cc.repository import IFileRepository from monkey_island.cc.resources.pba_file_download import PBAFileDownload -FILE_NAME = "test_file" -FILE_CONTENTS = b"HelloWorld!" - - -class MockFileRepository(IFileRepository): - def __init__(self): - self._file = io.BytesIO(FILE_CONTENTS) - - def save_file(self, unsafe_file_name: str, file_contents: BinaryIO): - pass - - def open_file(self, unsafe_file_name: str) -> BinaryIO: - if unsafe_file_name != FILE_NAME: - raise FileRetrievalError() - - return self._file - - def delete_file(self, unsafe_file_name: str): - pass - - def delete_all_files(self): - pass - @pytest.fixture def flask_client(build_flask_client): @@ -56,3 +31,11 @@ def test_file_download_endpoint_404(tmp_path, flask_client): resp = flask_client.get(download_url) assert resp.status_code == 404 + + +def test_file_download_endpoint_500(tmp_path, open_error_flask_client): + download_url = get_url_for_resource(PBAFileDownload, filename="test") + + resp = open_error_flask_client.get(download_url) + + assert resp.status_code == 500 diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_upload.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_upload.py index 2642c0758..e4cb5a487 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_upload.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_pba_file_upload.py @@ -1,12 +1,10 @@ -import io -from typing import BinaryIO - import pytest from tests.common import StubDIContainer +from tests.monkey_island import SingleFileRepository from tests.unit_tests.monkey_island.conftest import get_url_for_resource from tests.utils import raise_ -from monkey_island.cc.repository import FileRetrievalError, IFileRepository +from monkey_island.cc.repository import IFileRepository from monkey_island.cc.resources.pba_file_upload import LINUX_PBA_TYPE, WINDOWS_PBA_TYPE, FileUpload TEST_FILE_CONTENTS = b"m0nk3y" @@ -40,28 +38,9 @@ def mock_get_config_value(monkeypatch): ) -class MockFileRepository(IFileRepository): - def __init__(self): - self._file = None - - def save_file(self, unsafe_file_name: str, file_contents: BinaryIO): - self._file = io.BytesIO(file_contents.read()) - - def open_file(self, unsafe_file_name: str) -> BinaryIO: - if self._file is None: - raise FileRetrievalError() - return self._file - - def delete_file(self, unsafe_file_name: str): - self._file = None - - def delete_all_files(self): - self.delete_file("") - - @pytest.fixture def file_repository(): - return MockFileRepository() + return SingleFileRepository() @pytest.fixture @@ -119,6 +98,15 @@ def test_pba_file_upload_get__file_not_found(flask_client, pba_os, mock_get_conf assert resp.status_code == 404 +@pytest.mark.parametrize("pba_os", [LINUX_PBA_TYPE, WINDOWS_PBA_TYPE]) +def test_file_download_endpoint_500(open_error_flask_client, pba_os): + url = get_url_for_resource(FileUpload, target_os=pba_os, filename="bobug_mogus.py") + + resp = open_error_flask_client.get(url) + + assert resp.status_code == 500 + + @pytest.mark.parametrize("pba_os", [LINUX_PBA_TYPE, WINDOWS_PBA_TYPE]) def test_pba_file_upload_endpoint( flask_client, pba_os, mock_get_config_value, mock_set_config_value 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 9e01b8365..f170b0865 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 @@ -25,34 +25,33 @@ def test_format_config_for_agent__credentials_removed(): def test_format_config_for_agent__ransomware_payload(): expected_ransomware_options = { - "ransomware": { - "encryption": { - "enabled": True, - "directories": { - "linux_target_dir": "/tmp/ransomware-target", - "windows_target_dir": "C:\\windows\\temp\\ransomware-target", - }, + "encryption": { + "enabled": True, + "directories": { + "linux_target_dir": "/tmp/ransomware-target", + "windows_target_dir": "C:\\windows\\temp\\ransomware-target", }, - "other_behaviors": {"readme": True}, - } + }, + "other_behaviors": {"readme": True}, } flat_monkey_config = ConfigService.format_flat_config_for_agent() assert "payloads" in flat_monkey_config - assert flat_monkey_config["payloads"] == expected_ransomware_options + assert flat_monkey_config["payloads"][0]["name"] == "ransomware" + assert flat_monkey_config["payloads"][0]["options"] == expected_ransomware_options assert "ransomware" not in flat_monkey_config def test_format_config_for_agent__pbas(): - expected_pbas_config = { - "CommunicateAsBackdoorUser": {}, - "ModifyShellStartupFiles": {}, - "ScheduleJobs": {}, - "Timestomping": {}, - "AccountDiscovery": {}, - } + expected_pbas_config = [ + {"name": "CommunicateAsBackdoorUser", "options": {}}, + {"name": "ModifyShellStartupFiles", "options": {}}, + {"name": "ScheduleJobs", "options": {}}, + {"name": "Timestomping", "options": {}}, + {"name": "AccountDiscovery", "options": {}}, + ] flat_monkey_config = ConfigService.format_flat_config_for_agent() assert "post_breach_actions" in flat_monkey_config @@ -93,32 +92,14 @@ def test_format_config_for_agent__propagation(): flat_monkey_config = ConfigService.format_flat_config_for_agent() assert "propagation" in flat_monkey_config - assert "targets" in flat_monkey_config["propagation"] assert "network_scan" in flat_monkey_config["propagation"] - assert "exploiters" in flat_monkey_config["propagation"] - - -def test_format_config_for_agent__propagation_targets(): - expected_targets = { - "blocked_ips": ["192.168.1.1", "192.168.1.100"], - "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], - "local_network_scan": True, - "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], - } - - flat_monkey_config = ConfigService.format_flat_config_for_agent() - - assert flat_monkey_config["propagation"]["targets"] == expected_targets - assert "blocked_ips" not in flat_monkey_config - assert "inaccessible_subnets" not in flat_monkey_config - assert "local_network_scan" not in flat_monkey_config - assert "subnet_scan_list" not in flat_monkey_config + assert "exploitation" in flat_monkey_config["propagation"] def test_format_config_for_agent__network_scan(): expected_network_scan_config = { "tcp": { - "timeout_ms": 3000, + "timeout": 3.0, "ports": [ 22, 80, @@ -136,7 +117,13 @@ def test_format_config_for_agent__network_scan(): ], }, "icmp": { - "timeout_ms": 1000, + "timeout": 1.0, + }, + "targets": { + "blocked_ips": ["192.168.1.1", "192.168.1.100"], + "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], + "local_network_scan": True, + "subnets": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], }, "fingerprinters": [ {"name": "elastic", "options": {}}, @@ -161,36 +148,63 @@ def test_format_config_for_agent__network_scan(): assert "finger_classes" not in flat_monkey_config +def test_format_config_for_agent__propagation_network_scan_targets(): + expected_targets = { + "blocked_ips": ["192.168.1.1", "192.168.1.100"], + "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], + "local_network_scan": True, + "subnets": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], + } + + flat_monkey_config = ConfigService.format_flat_config_for_agent() + + assert flat_monkey_config["propagation"]["network_scan"]["targets"] == expected_targets + assert "blocked_ips" not in flat_monkey_config + assert "inaccessible_subnets" not in flat_monkey_config + assert "local_network_scan" not in flat_monkey_config + assert "subnet_scan_list" not in flat_monkey_config + + def test_format_config_for_agent__exploiters(): expected_exploiters_config = { "options": { "http_ports": [80, 443, 7001, 8008, 8080, 9200], }, "brute_force": [ - {"name": "MSSQLExploiter", "supported_os": ["windows"], "options": {}}, - {"name": "PowerShellExploiter", "supported_os": ["windows"], "options": {}}, - {"name": "SSHExploiter", "supported_os": ["linux"], "options": {}}, + {"name": "MSSQLExploiter", "options": {}}, + { + "name": "PowerShellExploiter", + "options": {}, + }, + {"name": "SSHExploiter", "options": {}}, { "name": "SmbExploiter", - "supported_os": ["windows"], "options": {"smb_download_timeout": 30}, }, { "name": "WmiExploiter", - "supported_os": ["windows"], "options": {"smb_download_timeout": 30}, }, ], "vulnerability": [ - {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, - {"name": "Log4ShellExploiter", "supported_os": ["linux", "windows"], "options": {}}, - {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}, + { + "name": "HadoopExploiter", + "options": {}, + }, + { + "name": "Log4ShellExploiter", + "options": {}, + }, + { + "name": "ZerologonExploiter", + "options": {}, + }, ], } flat_monkey_config = ConfigService.format_flat_config_for_agent() assert "propagation" in flat_monkey_config - assert "exploiters" in flat_monkey_config["propagation"] + assert "exploitation" in flat_monkey_config["propagation"] - assert flat_monkey_config["propagation"]["exploiters"] == expected_exploiters_config + assert flat_monkey_config["propagation"]["exploitation"] == expected_exploiters_config assert "exploiter_classes" not in flat_monkey_config diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_representations.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_representations.py index c088c3dce..e40e4470f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_representations.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_representations.py @@ -1,4 +1,5 @@ from datetime import datetime +from enum import Enum from unittest import TestCase import bson @@ -44,3 +45,11 @@ class TestRepresentations(TestCase): } ), ) + + def test_normalize__enum(self): + class BogusEnum(Enum): + bogus_val = "Bogus" + + my_obj = {"something": "something", "my_enum": BogusEnum.bogus_val} + + assert {"something": "something", "my_enum": "bogus_val"} == normalize_obj(my_obj) diff --git a/pyproject.toml b/pyproject.toml index 10415750f..5f8e07b67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ log_cli_date_format = "%H:%M:%S" addopts = "-v --capture=sys tests/unit_tests" norecursedirs = "node_modules dist" markers = ["slow: mark test as slow"] +pythonpath = "./monkey" [tool.vulture] exclude = ["monkey/monkey_island/cc/ui/", "monkey/tests/", "monkey/monkey_island/docs/"] diff --git a/vulture_allowlist.py b/vulture_allowlist.py index d451f367d..e1d56f689 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -184,6 +184,19 @@ architecture # unused variable (monkey/infection_monkey/exploit/caching_agent_r response_code # unused variable (monkey/monkey_island/cc/services/aws/aws_command_runner.py:26) release_convention # unused method (monkey/common/di_container.py:174) +# Agent Configuration +_make_plugin_configuration # unused method (monkey/common/configuration/agent_configuration.py:19) +_make_custom_pba_configuration # unused method (monkey/common/configuration/agent_configuration.py:24) +_make_exploiter_configuration # unused method (monkey/common/configuration/agent_configuration.py:69) +_make_exploitation_options_configuration # unused method (monkey/common/configuration/agent_configuration.py:86) +_make_scan_target_configuration # unused method (monkey/common/configuration/agent_configuration.py:105) +_make_icmp_scan_configuration # unused method (monkey/common/configuration/agent_configuration.py:107) +_make_tcp_scan_configuration # unused method (monkey/common/configuration/agent_configuration.py:122) +_make_network_scan_configuration # unused method (monkey/common/configuration/agent_configuration.py:110) +_make_propagation_configuration # unused method (monkey/common/configuration/agent_configuration.py:167) +_make_agent_configuration # unused method (monkey/common/configuration/agent_configuration.py:192) + + # TODO DELETE AFTER RESOURCE REFACTORING NetworkMap Arc.dst_machine